做一个在线教学平台,最让人头疼的往往不是业务逻辑多复杂,而是“东西太多,理不清”。从用户角色、课程体系,到作业考试、数据统计,每一块都牵扯着前后端的数据流转。我见过太多人一上来就写代码,结果写到一半发现表结构不合理,又推倒重来。如果你正准备用SpringBoot+Vue这套组合去实现一个信息化在线教学平台,或者你在为课设、毕设、面试项目找一个可以“抄作业”的完整参考,我强烈建议你先花十分钟把这篇文章看完。我会把整个系统的设计思路、技术选型逻辑、核心功能拆解、数据库设计细节、前后端实现过程,以及我在实际开发中踩过的一堆坑,都掰开了讲清楚。
我当初接手这个项目时,手里只有标题里的几个关键词:SpringBoot、Vue、Java、MySQL、MyBatis。这几个词组合在一起,基本就定死了技术方向——这是一个典型的前后端分离项目,后端用SpringBoot+MyBatis操作MySQL,前端用Vue框架做单页应用。但“怎么做”和“做成什么样”,需要自己往下挖。今天我不打算给你贴一堆零散的代码片段,而是按我自己的实操顺序,把这个项目的完整实现路径从头到尾捋一遍。
1. 项目整体设计与思路拆解
1.1 一份标题背后的真实需求:这个系统到底要管什么
先说结论:这个系统不是简单把线下的课表搬到线上,而是要覆盖“教、学、管、评”四个核心动作。我们打开“信息化在线教学平台”这几个字,拆开看。
- 在线教学:说明核心业务是课程的交付,包括课程信息的展示、教学资源的传递、在线作业与考试。
- 平台:意味着它有多方角色共存,最典型的是学生、教师、管理员三种身份,各自有独立的操作边界。
- 管理:说明系统需要有后台维度,管理员能管用户、管课程分类、管全局数据;教师能管自己的课程和学生;学生能看到自己的学习进度和成绩。
如果从面向对象的视角去抽象,整个系统的核心实体至少包括:用户(User)、课程(Course)、课程分类(Category)、选课关系(StudentCourse)、教学资源(Resource)、作业(Homework)、作业提交记录(SubmitRecord)、考试(Exam)、考试题目(Question)、成绩(Score)。再往外扩一点,还有公告(Notice)、评论(Comment)、学习日志(StudyLog)等辅助模块。
我见过很多半成品项目只做了用户登录和课程列表,那叫“管理系统”,不叫“教学平台”。你既然要做完整源码级别的东西,那么这些模块必须形成一个闭环:学生选课 → 看资源 → 做作业 → 参加考试 → 看到成绩 → 教师批改并统计。这个闭环里,任何一环缺失,项目在答辩或实际演示时都会露馅。
1.2 技术选型的底层逻辑:我为什么坚持SpringBoot+Vue这套组合
说实话,Java生态里做Web后端,能选的东西太多了:SSM、SpringBoot、SpringCloud,甚至还有人用JFinal。但在这个项目里,SpringBoot是性价比最高的选择,没有之一。
SpringBoot带来的最大价值是“约定优于配置”。它通过自动配置把SpringMVC、MyBatis、事务管理、连接池这些底层细节全部封装好,你不需要写一堆XML配置文件去定义Bean,也不需要操心Tomcat怎么集成——内嵌容器,直接java -jar就能跑。这个特性对教学平台这种“业务复杂度高、技术架构要求快”的项目来说非常合适。你可以把精力集中在业务代码上,而不是浪费在环境配置上。
前端用Vue,核心原因是组件化和数据驱动。在线教学平台有大量可复用的界面片段:课程卡片、用户列表、表单弹窗、分页组件。用原生JS或jQuery去写这些,光DOM操作就能让人崩溃。Vue的核心思路是把页面拆成组件,数据变化时视图自动更新,这正好对得上管理后台那堆“表格+表单+弹窗”的典型交互场景。
持久层选择MyBatis,理由更实在:SQL可控性强。像“统计某门课程的平均分、及格率”“查询选课人数最多的课程Top10”这类稍微复杂的统计需求,MyBatis可以让你直接写SQL,想怎么优化就怎么优化。相比之下,JPA/Hibernate虽然省事,但在复杂查询和SQL调优上反而会成为一种束缚。做这类系统,表关系复杂、查询条件动态变化,MyBatis的动态SQL标签(if、where、foreach)几乎是量身定做的工具。
MySQL没什么好争议的——免费、稳定、资料多、够用。单机版教学平台的数据量撑死几十万条,MySQL 5.7或8.0完全扛得住,而且和SpringBoot+MyBatis的生态整合资料最多,遇到问题随手一搜就有答案。
特别提醒:如果你的SpringBoot版本是2.7以上,选MyBatis依赖时建议检查版本兼容性。SpringBoot 3.x要求JDK 17+,而很多教学项目还在用JDK 8,如果你不想给自己找麻烦,SpringBoot 2.5~2.7 + JDK 1.8是最稳妥的组合,网上资料也最多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 数据库设计的关键决策:宁可多画一张关系图,也别急着敲代码
这是整个项目里最不能省的一步。我在动手写任何@Mapper接口之前,花了整整一天画ER图和表结构设计文档。为什么这么较真?因为这个系统的表关系比想象中复杂,它不止是简单的单表CRUD。
我们梳理一下核心表及其关系:
- 用户表(user):字段至少有
id、username、password、real_name、role(0管理员/1教师/2学生)、email、phone、avatar、status、create_time。角色字段我建议用tinyint存数字,不要直接存字符串,既省空间又方便扩展。 - 课程表(course):
id、course_name、cover(封面图URL)、description、teacher_id(关联user表)、category_id(关联分类表)、start_time、end_time、status。 - 选课表(student_course):这是典型的中间表,
id、student_id、course_id、select_time。唯一约束必须加,UNIQUE KEY uk_student_course(student_id, course_id),否则用户重复点击选课,会产生脏数据。 - 资源表(resource):
id、course_id、title、file_url、file_type(视频/文档/图片)、file_size、uploader_id、upload_time。 - 作业表(homework):
id、course_id、title、content、deadline、create_time。 - 提交记录表(submit_record):
id、homework_id、student_id、submit_content、file_url、submit_time、score、comment。
这个设计里有几个容易踩坑的点,我必须单独拿出来讲。
第一个坑:外键到底加不加。 我的经验是不加物理外键,只加普通索引。原因是教学平台的数据会涉及批量导入、逻辑删除,物理外键在删除课程时会因为选课表的存在而报错,处理起来非常痛苦。你只需要在student_course.course_id、resource.course_id等字段上建普通索引(KEY idx_course_id(course_id))就足够了,关联关系交给Service层去维护。
第二个坑:成绩字段怎么存。 作业提交记录里的score字段,一定要用DECIMAL(5,2),不要用DOUBLE或FLOAT。浮点数在计算总和、平均值时会有精度丢失问题,考试成就感瞬间崩塌。MySQL的DECIMAL是固定精度,存分数这种小数点后最多两位的数据刚刚好。
第三个坑:逻辑删除统一处理。 给每张核心业务表都加上deleted字段(tinyint,默认0)。用户误删课程、管理员下架资源,都不需要物理删除,只需要置1。这个习惯在面试聊项目时是一个很大的加分项,因为它体现了数据安全意识。
2.2 权限认证与安全方案:Session还是JWT?这决定了前端和后端的协作方式
在线教学平台涉及三种角色,权限控制是必须的。我见过有人用Session+拦截器做,也有人用Shiro、Spring Security,但在这个前后端分离的项目里,我推荐用JWT + 拦截器这条路,理由很简单:前后端分离后,后端接口是无状态的,Session跨域要处理CORS和Cookie问题,复杂度远高于JWT。
具体落地方案是这样:
- 用户登录成功后,后端用JWT工具类生成一个token,把
userId、username、role塞进token的payload里,签好名返回给前端。 - 前端把token存在
localStorage或Pinia/Vuex的store里,每次请求在axios拦截器里把token塞进Authorization请求头。 - 后端写一个
JwtInterceptor拦截器,配置在WebMvcConfigurer中,对/api/**路径下的请求统一校验token。校验通过就把userId放进ThreadLocal,方便后续控制器直接取当前登录人。 - 角色控制方面,可以在拦截器里校验请求路径对应的角色要求,也可以在Controller方法上用自定义注解(比如
@RequireRole("teacher"))配合切面实现。
代码示例(JWT工具类核心方法):
java复制public String generateToken(Long userId, String username, Integer role) {
// HS512签名,密钥从配置文件中读取,不要硬编码
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
return Jwts.builder()
.setSubject(username)
.claim("userId", userId)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)) // 2小时有效
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
密码加密这块必须用BCrypt,不要用MD5或SHA1。MD5撞库太容易了,你在简历上写“安全的密码存储方案”,结果用MD5,面试官一眼就看穿。Spring Security的BCryptPasswordEncoder可以直接拿出来当工具类用,即使不引入全套Security框架,也可以单独依赖spring-security-crypto,按下面的方式处理:
java复制BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 加密存储
String encodedPwd = encoder.encode(rawPassword);
// 校验密码
boolean matches = encoder.matches(rawPassword, encodedPwd);
2.3 文件上传与在线播放的实施方案:本地路径还是OSS?先想清楚再说
在线教学平台必然涉及上传视频、课件、作业附件。最常见的做法有两种:本地磁盘存储和云OSS。
如果只是课设/毕设级别的项目,我建议直接存本地。 在配置文件里指定一个upload.path,比如D:/upload/(Windows)或/opt/upload/(Linux),然后把文件写到这个目录下,数据库里只存访问的相对路径/files/20240101/xxx.mp4。再写一个本地文件映射配置,把/files/**请求映射到磁盘目录。这样做的优点是:不受云厂商限制,离线也能跑通;缺点是:扩展性和备份不方便。但应付演示和中小规模使用完全够了。
如果要做在线播放视频,一定要提前考虑播放格式的处理。 我在开发时遇到一个很经典的坑:在Chrome里,mp4格式的视频文件,只有少数编码(H.264)能直接播放,mkv、avi格式的浏览器无法解码。我的处理方案是:在后台上传视频时,限制只允许上传mp4格式,并在前端播放器(比如video.js)里做好兼容。更进一步,如果需要兼容切片播放(m3u8),可以用FFmpeg把视频切片成HLS流,前端用video.js配合videojs-contrib-hls插件播放。这一步是很多在线教学平台项目没有做到位的,你做到了,就是亮点。
上传文件的接口实现,建议采用分片上传的简化版。 如果文件很大(超过几百MB),直接一次性上传很容易超时。你可以先把文件上传大小上限调到合理范围(比如200MB),然后用MultipartFile接收,存到本地后返回文件URL。如果真想加个亮点,可以用CommonsMultipartResolver做断点续传——这个可以放到改进方向里去讲,核心功能跑通更重要。
3. 实操过程与核心环节实现
3.1 环境准备与版本选型:一套不折腾的组合
这个项目要跑起来,你本机需要准备这些东西,我把版本号和注意事项一并列出来:
| 工具 | 推荐版本 | 关键注意点 |
|---|---|---|
| JDK | 1.8(8u202以上) | 不要用JDK 9~11的中间版本,兼容性最稳的是8 |
| Maven | 3.6.3+ | 配置阿里云镜像,否则依赖下载能等到怀疑人生 |
| MySQL | 5.7或8.0 | 8.0注意驱动要配com.mysql.cj.jdbc.Driver |
| Node.js | 14.x或16.x | Vue CLI 4/5对Node版本有要求,太新反而报错 |
| IDE | IDEA 2021以上 | 装Lombok插件,否则@Slf4j注解会标红 |
| Vue | Vue 2.6及以上 | 和Element UI搭配,资料最多 |
工程结构我建议分成两个独立目录:backend(SpringBoot工程)和frontend(Vue工程)。不要用后端Maven把前端代码包进去,开发期分开跑,部署期再合并,逻辑最清晰。
后端项目初始化要点:用Spring Initializr生成工程时,别乱勾选依赖,Web、MyBatis、MySQL Driver、Lombok四件套就够了。校验相关的依赖(如validation)后续需要再手动加,省得自动生成的pom.xml引入一堆用不上的东西。application.yml里最核心的配置这样写:
yaml复制server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/edu_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.edu.entity
configuration:
map-underscore-to-camel-case: true
这里的map-underscore-to-camel-case: true特别重要,它可以把数据库的create_time自动映射到实体的createTime属性,省掉一堆ResultMap手动映射。
3.2 后端核心接口实现与MyBatis动态SQL:分页查询就是试金石
在线教学平台的后端接口,按模块分至少有:认证接口、用户管理接口、课程管理接口、选课接口、资源上传接口、作业管理接口、考试接口、统计接口。这么多接口,不可能全部细讲,我挑一个能反映设计水平的典型场景来讲:课程分页查询+多条件过滤。
这个接口的核心需求是:前端传pageNum、pageSize、courseName(课程名称模糊查)、categoryId(分类筛选)、teacherName(教师姓名)等可选条件,后端返回课程列表+总条数。用MyBatis实现时,Mapper接口这样定义:
java复制List<CourseVO> selectCoursePage(@Param("params") CourseQuery params, @Param("offset") int offset, @Param("limit") int limit);
long countCoursePage(@Param("params") CourseQuery params);
对应的XML里用动态SQL:
xml复制<select id="selectCoursePage" resultType="com.example.edu.vo.CourseVO">
SELECT c.*, u.real_name AS teacher_name, cat.name AS category_name
FROM course c
LEFT JOIN user u ON c.teacher_id = u.id
LEFT JOIN category cat ON c.category_id = cat.id
<where>
<if test="params.courseName != null and params.courseName != ''">
AND c.course_name LIKE CONCAT('%', #{params.courseName}, '%')
</if>
<if test="params.categoryId != null">
AND c.category_id = #{params.categoryId}
</if>
<if test="params.teacherName != null and params.teacherName != ''">
AND u.real_name LIKE CONCAT('%', #{params.teacherName}, '%')
</if>
AND c.deleted = 0
</where>
ORDER BY c.create_time DESC
LIMIT #{offset}, #{limit}
</select>
这里有几个值得注意的实操点:
- 一定要用
<where>标签,它会自动处理条件前面的AND,否则第一个条件为空时SQL会拼成WHERE AND ...直接报错。 - 分页方式我用的是
LIMIT offset, limit,这是最基础也是面试官最认的方式。要进阶的话,可以实现MyBatis的PageHelper插件,一行注解搞定分页,但建议先自己手写一次,理解原理再上框架。 LIKE查询记得用CONCAT('%', #{xxx}, '%'),不要直接在Java代码里拼好再传进来,防止SQL注入。
Service层事务边界也要想清楚。学生选课这个动作涉及两步:往student_course表插入记录,同时更新course表的选课人数(selected_count)字段。两步必须在一个事务里,否则会出现“选课记录有了但人数没加”的数据不一致问题。在Service方法上加@Transactional(rollbackFor = Exception.class)是底线。
3.3 前端页面开发与接口联调:路由、状态管理、组件复用
Vue前端这一步,最忌讳的是按页面去零散开发。我在动手之前,先定好了一套规范,这让我后续写页面速度快了非常多。
路由设计:采用动态路由思路,根据角色渲染菜单。router/index.js里配好基础路由(登录页、首页框架),再在全局守卫router.beforeEach里根据登录状态和角色决定放行还是强制跳转登录页。比如:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
next()
}
})
这个守卫是前端访问控制的最后一道防线。哪怕后端拦截器已经做了接口权限校验,前端也必须做路由权限控制,否则用户手动改URL就能绕过菜单直接进管理页面。
axios封装:我建议把axios实例单独放在utils/request.js里,统一设置baseURL(开发环境用代理转发)、请求超时时间、请求拦截器(注入token)、响应拦截器(统一处理HTTP 401跳登录页、后端业务码提示)。这样业务组件里调用接口,只需要写:
javascript复制export function getCourseList(data) {
return request({
url: '/course/page',
method: 'post',
data
})
}
核心页面拆解:课程管理页面是最典型的一个页面,包含搜索表单、表格、分页器、新增/编辑弹窗。Element UI的el-table、el-pagination、el-dialog组件可以快速搭建。我特别建议把“搜索表单+表格+分页”封装成一个通用组件PageTable,通过props传入查询函数和列配置,这样作业管理、用户管理、资源管理都能复用,代码量直接减半。
开发环境跨域:在vue.config.js里配好代理,否则本地开发时前端localhost:8081请求后端localhost:8080会被浏览器跨域拦截:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
这样前端代码里请求/api/course/page,实际上会代理到http://localhost:8080/api/course/page,cookie、header都能正常携带。
3.4 项目打包与部署的完整步骤:从本地跑通到服务器上线
部署阶段,我踩过的坑比开发阶段还多。这里直接给你一套实测稳的流程。
第一步:前端打包。在frontend目录下执行npm run build。打包前必须检查vue.config.js里的publicPath配置。如果你要把前端打包产物放进SpringBoot的static目录里用同一个端口访问,publicPath必须设为'./'(相对路径),部署后无论放在根目录还是子路径都能访问。如果是'/'绝对路径,而后端静态资源映射有问题,就会出现CSS、JS全部404的情况。
产物处理:把dist目录里的静态资源复制到SpringBoot的src/main/resources/static目录下。此时,后端Tomcat会优先查找static目录下的静态资源(如index.html),不过这里存在一个风险:如果你在写后端时定义了类似/**的拦截器,静态资源可能被拦截。解决方案是在SpringBoot的WebMvcConfigurer中单独放行静态资源路径:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/");
}
第二步:后端打包。在项目根目录执行mvn clean package -DskipTests,得到target目录下的jar包。启动命令:
bash复制java -jar edu-platform.jar --spring.profiles.active=prod
注意:如果服务器上的MySQL和你本地的MySQL版本不一致,比如本地是8.0、服务器是5.7,驱动配置里
serverTimezone=Asia/Shanghai必须保留,否则日期字段查询会报时区错误。另外,数据库初始化要指定字符集:CREATE DATABASE edu_platform DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,否则中文查询会出现乱码或“????”。
4. 常见问题与排查技巧实录
4.1 启动失败的三大高频原因
原因一:端口被占用。 报错信息通常是Web server failed to start. Port 8080 was already in use.。解决办法有两个:一是netstat -ano | findstr 8080找到占用进程,手动结束;二是在配置里换端口。我建议做系统的时候就养成server.port从配置文件读取的习惯,部署时用启动参数覆盖,避免改代码。
原因二:数据库连不上。 报错关键字是Access denied for user 'root'@'localhost'或者Communications link failure。前者是账号密码错误,后者大概率是MySQL服务没启动,或者URL里的IP、端口写错。排查时要学会看完整异常栈,不要只停在第一行。最常用的排查命令就是mysql -uroot -p在命令行里先验证能不能连上,把环境和代码问题分开。
原因三:Lombok找不到符号。 明明代码没问题,但编译报java: cannot find symbol 符号: 方法 getXxx()。这个一般是因为IDE没启用Annotation Processing,或者在IDEA里没装Lombok插件(新版IDEA已集成)。另外,Maven编译时如果JDK版本不对,也会出现这个问题,检查pom.xml里的java.version是否和本机一致。
4.2 MyBatis最容易踩的深坑
第一个坑:Mapper接口和XML文件映射不上。 报错Invalid bound statement (not found)。如果你确定Mapper接口方法存在且XML里写了,那问题大概率出在XML文件没有编译到classpath。检查target/classes/mapper/目录下有没有对应的XML文件,没有的话在pom.xml里加配置:
xml复制<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
第二个坑:参数传递问题。 多个参数时没有加@Param注解,报错Parameter 'xxx' not found. Available parameters are [0, 1, param1, param2]。解决办法就是像我前面写的那样,在Mapper接口方法里给每个参数加@Param("xxx")。更省事的方案是定义一个QueryDTO对象,把多个查询条件封装进去,一个参数传到底,既清晰又好维护。
第三个坑:动态SQL的if条件判空。 很多人习惯写<if test="courseName != null and courseName != ''">,这没问题,但对于status这种整型字段,它会默认等于0,此时status != ''在MyBatis的OGNL表达式里会判断为true还是false?实测整型0和空字符串比较是true,导致本意为“不查status字段”的情况变成了“查status=0”。解决办法是只有String类型字段才同时判空,Integer字段只判!= null即可。
4.3 前端跨域和生产环境API地址问题
我见过很多项目,本地开发好好的,一打包部署就白屏或请求404。核心原因有两个。
第一个:开发时用Vue代理,打包后代理不生效。 开发环境vue.config.js里的devServer代理只在开发服务器里有效,打包后的index.html是静态文件,它没有代理能力。所以你在代码里写的请求路径如果是/api/course/page,打包后部署到Nginx或Tomcat时,这个/api前缀必须真的能在服务端解析到。解决办法是用Nginx时加一层location /api/ { proxy_pass http://localhost:8080; },或者在写axiosbaseURL时用环境变量区分:
javascript复制// 开发环境用 '/api',生产环境直接用后端地址
const baseURL = process.env.NODE_ENV === 'development' ? '/api' : 'http://47.xxx.xxx.xxx:8080'
第二个:打包后页面刷新404。 如果你的项目用了Vue Router的history模式,服务器没有配置try_files,刷新页面就会404。稳妥的做法是改用hash模式,URL上会多个#,颜值略低但绝对不死。或者在后端加一个Controller,将所有非/api的路径都forward到index.html。考虑到教学平台是工具型系统,直接选hash模式最省心。
4.4 数据库连接与性能排查:让项目演示不卡顿的关键
在线教学平台演示时最尴尬的场景是:点一个列表,转圈转半天。这里分享两个我实测有效的优化动作。
动作一:连接池参数要配。 SpringBoot默认的HikariCP连接池是很优秀的,但它有默认值必须优化。比如数据库最大连接数默认10,并发场景不够用,可以配:
yaml复制spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
动作二:核心表必须建索引。 查询频率最高的搜索场景是course表的course_name模糊查询和category_id精确查询。很多人建表时不建索引,导致数据一多全表扫描。以下索引是必建的:
sql复制ALTER TABLE course ADD INDEX idx_course_name (course_name);
ALTER TABLE course ADD INDEX idx_category_id (category_id);
ALTER TABLE student_course ADD INDEX idx_student_id (student_id);
建完之后,用EXPLAIN SELECT ...查看执行计划,如果看到type列出现ALL,说明是全表扫描,必须加索引优化。
动作三:统计报表用SQL聚合,别在Java里算。 比如统计每门课的选课人数、平均分、及格率,直接在Mapper XML里写一句GROUP BY就搞定,不要在Service里循环查询。我在最初版本里犯过这个错误——一次统计要查询N次课程表,平台数据量一涨直接卡死。后来改成一条SQL之后,从秒级降到毫秒级。
写在最后:一次真实项目中的版本演进
这个在线教学平台我从第一版到最终交付,中间迭代了三次。第一版是最原始的单体JSP项目,后来因为前端体验太差,彻底改造成前后端分离的SpringBoot+Vue架构。改造过程中,最痛的不是技术选型,而是数据模型的重构——第一版里把作业和考试混在一张表里,导致后续统计极其困难,第二版才拆成homework和exam独立模块,才真正把“教、学、管、评”闭环跑通。
如果一定要给后来者一个经验总结,我会说:做这类系统,先想清楚角色和流程,再设计表和接口,最后才动手写业务代码。 技术上的SpringBoot、Vue、MyBatis都是成熟的工具,真正拉开差距的是你如何组织业务结构、如何设计数据关系、如何处理边界场景。我当初在数据库设计上多花的那个星期,在后来的开发过程中帮我省下了至少三倍的时间——这一笔账,怎么算都划算。
