ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue课程答疑系统开发实战:从数据库设计到前后端实现

SpringBoot+Vue课程答疑系统开发实战:从数据库设计到前后端实现 1. 项目概述与技术选型思考做课程答疑系统这套东西最核心的需求其实很简单学生在学习过程中遇到问题需要有个地方能提问老师或者助教能及时解答并且这些问答的沉淀还能反哺后来的学习者。听起来不复杂但真正落地的时候你会发现纯粹的“提问—回答”只是冰山一角背后的用户角色管理、问题状态流转、内容审核、消息通知、学习资料的关联推荐每一块都需要仔细打磨。这套源码采用的是 SpringBoot Vue MyBatis MySQL 的组合放在 2025 年的视角下看依然是非常经典且实用的选型。SpringBoot 负责后端服务的快速搭建和业务逻辑处理Vue 负责前端交互界面的开发MyBatis 负责数据库的 ORM 映射和 SQL 控制MySQL 则是数据存储的底座。为什么不用 MyBatis-Plus为什么不用 PostgreSQL这是我决定深聊这个项目之前想先和大家说清楚的问题。1.1 技术栈组合的底层逻辑先用一句话概括这套技术栈是**“稳定性优先 学习曲线平缓 生态成熟”**的典型代表。先说 SpringBoot。它解决了传统 Spring 项目配置繁琐的问题通过自动配置和 starter 机制让开发者只需要关注业务代码本身。课程答疑系统这种中后台管理系统涉及用户认证、权限控制、数据持久化、接口暴露SpringBoot 的生态里都有非常成熟的解决方案。比如 Spring Security 做登录认证Spring Validation 做参数校验Spring Cache 做缓存抽象这些都可以无缝集成。对于做课程平台、教育机构系统或者毕业设计、企业内部培训系统的人来说SpringBoot 是最不容易出错的选择。再说 Vue。Vue 的核心优势在于响应式数据绑定和组件化开发。课程答疑系统的前端页面通常包含提问列表、问题详情、个人中心、后台管理等多个视图组件化的思路可以把这些页面拆分成可复用的模块比如问题卡片组件、回答列表组件、富文本编辑器组件开发效率和维护性都显著提升。Vue 的生态里还有 Vue Router 做路由管理、Pinia 或 Vuex 做状态管理、Element UI 或 Ant Design Vue 做 UI 组件库组合起来几乎可以覆盖所有中后台场景。然后是 MyBatis。这可能是很多人在选型时纠结的地方——为什么不用 JPA我的看法是答疑系统这种业务SQL 往往带有复杂的多表关联查询和动态条件拼接比如“查询某个学生所有未解决的问题按课程分类同时统计每个分类下的回答数量”。MyBatis 的动态 SQL 能力在这种场景下非常灵活XML 文件里的 SQL 一目了然便于 DBA 审查和调优。而且 MyBatis 对 SQL 的控制是绝对的不会有 JPA 那种“自动化生成 SQL 导致性能不可控”的隐患。最后是 MySQL。它依然是中小型项目的数据存储首选。事务支持成熟、索引机制完善、社区资料丰富遇到任何问题都能搜到解决方案。对于课程答疑系统这种数据量级一般来说也就是几万到几十万条问答数据MySQL 配合合理的索引设计和分页策略性能完全够用。1.2 这个项目适合谁学习和深入我得坦白说这套源码的学习价值在于它的“完整性”和“规范性”而不是“技术先进性”。非常适合下面这三类人群第一类是在校学生或应届毕业生。课程答疑系统是典型的毕业设计选题因为它涵盖了一个完整 Web 系统所需的全部要素——前端页面、后端接口、数据库设计、权限控制、文件上传、分页查询等。把这套源码吃透相当于把大学四年学的知识串联了一遍面试时也有的聊。第二类是初入行的 Java 开发工程师。很多人在公司里做的都是某个模块的维护工作缺乏从零搭建一个完整系统的经验。这套源码能帮你理解项目的整体架构、模块划分、接口设计规范尤其是前后端分离的开发模式能让你快速适应企业级开发流程。第三类是有实际业务需求的技术人员。比如培训机构要做一个内部答疑平台或者高校老师需要一个课程问答工具这套源码可以直接二次开发省去从零搭建的重复劳动。2. 核心功能模块拆解与数据库设计课程答疑系统听起来只是“提问—解答”但真正设计功能模块时你会发现它必须覆盖四个层面的需求用户层、业务层、管理层、数据层。我以前做过一个在线教育平台刚开始只做了提问和回答两个功能上线后学生大量灌水、重复提问、无人响应最后不得不返工补了审核和指派功能。所以这次梳理这套源码时我会特别强调功能设计的完整性。2.1 六大核心功能模块解析第一个模块是用户认证与权限管理。系统必须区分三种角色学生、教师、管理员。学生可以提问、回答、收藏问题教师可以回答问题、将问题标记为“已解决”、管理自己的课程分类问题管理员负责用户管理、问题审核、数据统计、敏感词过滤。SpringBoot 后端通过拦截器或 Spring Security 实现 token 鉴权Vue 前端通过路由守卫控制页面访问权限。第二个模块是问题发布与管理。学生发布问题时需要填写标题、问题描述、所属课程分类、标签等字段。这里有个容易忽略的细节问题描述通常是富文本内容所以系统需要集成富文本编辑器组件并且后端要做好 XSS 过滤和 HTML 标签白名单校验防止恶意脚本注入。第三个模块是回答与互动体系。教师和学生都可以对问题发布回答回答支持点赞、踩。注意回答的排序逻辑——默认按“最佳答案”优先其次是点赞数最后按时间倒序。这就需要在数据库设计时预留 is_accepted、like_count 字段并在查询时做多级排序。第四个模块是搜索与筛选。根据标题模糊搜索、按课程分类筛选、按状态筛选未解决/已解决/已关闭、按热度排序。后端对应的是 MyBatis 动态 SQL根据传入的条件拼接 WHERE 子句。第五个模块是消息通知。当问题被回答时提问者需要收到通知当学生的问题被教师标记为“已解决”时也会触发系统消息。实现方案有两种一种是通过 WebSocket 推送实时通知另一种是通过数据库轮询或定时任务生成站内信。这套源码里用的是站内信方式实现简单、稳定性好。第六个模块是数据统计后台。管理员可以查看每日提问数量、回答问题率、活跃用户排行、课程分类热度等指标用图表形式在前端展示。后端对应的是聚合查询 SQL用 GROUP BY 和 COUNT 组合实现。2.2 数据库表设计与核心字段说明这部分我直接给出核心表的字段设计思路大家在二次开发时可以作为参考。用户表sys_user主键 id、用户名 username、密码 passwordBCrypt 加密存储、昵称 nickname、角色 role0-管理员1-教师2-学生、头像 avatar、创建时间 create_time。课程分类表course_category主键 id、分类名称 category_name、父级分类 parent_id支持二级分类、排序 sort、创建时间 create_time。问题表question主键 id、提问人 user_id、所属分类 category_id、标题 title、内容 contentTEXT 类型存储富文本 HTML、状态 status0-待解答1-已解决2-已关闭、浏览量 view_count、点赞数 like_count、创建时间 create_time、更新时间 update_time。这里建议在 category_id 和 status 上建立联合索引因为列表页最常见的查询条件是“某分类下所有待解答的问题”。回答表answer主键 id、所属问题 question_id、回答人 user_id、内容 content、是否最佳答案 is_accepted0-否1-是、点赞数 like_count、创建时间 create_time。同样的question_id 必须建索引否则回答列表查询会非常慢。消息通知表notification主键 id、接收人 user_id、通知类型 notify_type1-新回答2-问题解决3-系统公告、内容 content、是否已读 is_read、创建时间 create_time。我特别想强调一个问题表设计时一定要考虑数据删除的善后。比如用户被删除后他发布的提问和回答怎么处理是级联删除还是保留但匿名字段置空如果直接 DELETE 关联数据会造成历史问答链条断裂影响其他用户体验。合理的方案是逻辑删除——表中增加 deleted 字段0-正常1-删除查询时统一过滤。3. 后端落地实操SpringBoot MyBatis 核心环节实现现在聊到后端实现这部分是这套源码的骨架。我会拆成三层来讲项目初始化与配置、MyBatis 核心配置与使用、核心业务流程实现。每一步都会给出关键代码和配置并解释为什么这么做。3.1 项目初始化与关键配置文件创建 SpringBoot 项目时我建议直接用 Spring Initializrstart.spring.io生成基础骨架选择以下依赖Spring Web、MyBatis Framework、MySQL Driver、Lombok、Spring Validation。版本选择上SpringBoot 2.7.x 或 3.x 均可但要注意 MyBatis 对 SpringBoot 3 的兼容性推荐使用 mybatis-spring-boot-starter 2.3.x 以上版本。核心配置文件 application.yml 里需要注意几个关键项spring: datasource: url: jdbc:mysql://localhost:3306/qasystem?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mvc: hiddenmethod: filter: enabled: true mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.qasystem.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true有几个细节我得单独说一下数据库连接参数useSSLfalse 是因为本地开发环境通常没有配置 SSL 证书如果设为 true 反而可能导致连接报错。serverTimezoneAsia/Shanghai 必须设置否则 JDBC 驱动版本在 8.0 以上时会出现时区报错。map-underscore-to-camel-case设置为 true可以自动将数据库字段 create_time 映射到 Java 属性 createTime省去大量手动映射配置。这是 MyBatis 开发中提升效率的重要一环。log-impl设置为 StdOutImpl在开发阶段可以方便地在控制台查看 SQL 执行日志便于排查问题。生产环境记得关闭或者调整日志级别。再补充一个很容易踩坑的点如果使用 PageHelper 分页插件mybatis 配置中的 mapper-locations 路径必须和实际存放 Mapper XML 文件的目录保持一致否则启动时就会报 Invalid bound statement (not found) 错误。3.2 MyBatis 核心映射与动态 SQL 实战答疑系统的核心查询场景是问题列表的分页查询支持多条件组合筛选。我用一个实际案例来说明 MyBatis 动态 SQL 的写法。先定义实体类 QuestionData public class Question { private Integer id; private Integer userId; private Integer categoryId; private String title; private String content; private Integer status; private Integer viewCount; private Integer likeCount; private Date createTime; private Date updateTime; // 关联字段非数据库表字段 private String nickname; private String categoryName; }对应的 Mapper 接口public interface QuestionMapper { ListQuestion selectQuestionList(Param(question) Question question, Param(startTime) String startTime, Param(endTime) String endTime); }对应的 Mapper XMLselect idselectQuestionList resultTypecom.example.qasystem.entity.Question SELECT q.*, u.nickname, c.category_name FROM question q LEFT JOIN sys_user u ON q.user_id u.id LEFT JOIN course_category c ON q.category_id c.id where if testquestion.title ! null and question.title ! AND q.title LIKE CONCAT(%, #{question.title}, %) /if if testquestion.categoryId ! null AND q.category_id #{question.categoryId} /if if testquestion.status ! null AND q.status #{question.status} /if if teststartTime ! null and startTime ! AND q.create_time gt; #{startTime} /if if testendTime ! null and endTime ! AND q.create_time lt; #{endTime} /if /where ORDER BY q.create_time DESC /select为什么这里要用where标签而不是手动拼 WHERE因为where标签会自动处理第一个条件前面的 AND 关键字当所有条件都为空时还会自动去掉 WHERE避免 SQL 语法错误。这是 MyBatis 动态 SQL 最实用的特性之一。left join 关联用户表和分类表是为了在列表中直接展示提问人昵称和课程分类名避免在前端发多次请求。实际开发中这种关联查询是常态要注意区分“业务表”和“关联查询用表”合理利用索引。3.3 业务层事务控制与异常处理答疑系统中有一个典型场景需要特别注意事务控制当学生发布一个问题时需要同时执行“插入问题记录”和“更新用户提问数量统计”两个操作。如果第二个操作失败而第一个成功就会造成数据不一致。在 SpringBoot 中解决这个问题很简单在业务方法上添加Transactional注解Transactional(rollbackFor Exception.class) public void createQuestion(Question question) { questionMapper.insert(question); userStatisticsMapper.increaseQuestionCount(question.getUserId()); }注意rollbackFor Exception.class这个属性。Spring 的声明式事务默认只在遇到 RuntimeException 时回滚遇到受检异常如 IOException不会自动回滚。如果不显式指定 rollbackFor业务代码中抛出受检异常时事务不会回滚数据就会产生不一致。这是很多初学者容易忽略的细节。业务层还应该做参数校验。比如发布问题时标题不能为空、内容不能少于 10 个字符、课程分类必须存在这些校验可以在 Controller 层通过 Spring Validation 注解实现PostMapping(/question) public Result createQuestion(RequestBody Valid Question question, BindingResult result) { if (result.hasErrors()) { return Result.error(result.getFieldError().getDefaultMessage()); } return questionService.createQuestion(question); }实体类上对应的注解Data public class Question { NotBlank(message 标题不能为空) Size(max 100, message 标题长度不能超过100个字符) private String title; NotBlank(message 问题内容不能为空) Size(min 10, message 问题内容不能少于10个字符) private String content; NotNull(message 课程分类不能为空) private Integer categoryId; }这样设计的价值在于把参数校验逻辑从业务代码中剥离Controller 层只负责接收参数和返回结果Service 层只关注核心业务逻辑代码清晰且易于维护。4. 前端落地实操Vue 核心环节实现前端的核心任务是实现页面交互和接口对接。我会从工程化配置、核心页面逻辑、接口封装三个角度来梳理。4.1 Vue 工程化配置与目录结构使用 Vite 创建 Vue 3 项目是当前的主流方式。相比 Vue CLIVite 启动速度更快构建性能更好而且配置更简洁。创建项目命令npm create vitelatest qa-frontend -- --template vue安装核心依赖npm install vue-router4 pinia axios element-plus wangeditor/editor项目目录结构建议按以下方式组织src/ ├── api/ # 接口请求模块 │ ├── question.js # 问题相关接口 │ ├── user.js # 用户相关接口 │ └── statistics.js # 统计相关接口 ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── QuestionCard.vue # 问题卡片组件 │ └── AnswerList.vue # 回答列表组件 ├── router/ # 路由配置 │ └── index.js ├── stores/ # Pinia 状态管理 │ ├── user.js # 用户状态 │ └── app.js # 应用状态 ├── views/ # 页面组件 │ ├── Home.vue # 首页-问题列表 │ ├── QuestionDetail.vue # 问题详情页 │ ├── AskQuestion.vue # 发布问题页 │ ├── Profile.vue # 个人中心 │ └── admin/ # 管理后台页面 └── main.js4.2 路由守卫与权限控制权限控制是前端不可或缺的环节。虽然真正的数据安全靠后端接口鉴权保证但前端路由守卫能优化用户体验——未登录用户访问需要权限的页面时直接跳转到登录页而不是等接口返回 401 才处理。// router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token); const userRole localStorage.getItem(userRole); if (to.meta.requiresAuth !token) { next(/login); return; } if (to.meta.requiresAdmin userRole ! 0) { next(/); return; } next(); });代码中的关键是 to.meta 配置。在每个路由定义里通过 meta 标记该页面所需的权限级别{ path: /admin, component: AdminLayout, meta: { requiresAuth: true, requiresAdmin: true }, children: [ { path: questions, component: QuestionManage }, { path: users, component: UserManage }, ] }4.3 axios 封装与接口对接axios 封装是前端工程化的基础环节。项目里所有接口请求都走一个统一的实例便于集中处理 token 附带、错误提示、401 跳转等逻辑。// api/request.js import axios from axios; import { ElMessage } from element-plus; import router from /router; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器附加 token 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.message); return Promise.reject(new Error(res.message)); } return res; }, error { if (error.response?.status 401) { localStorage.removeItem(token); router.push(/login); ElMessage.error(登录已过期请重新登录); } else { ElMessage.error(error.message); } return Promise.reject(error); } ); export default request;这里有两个细节要注意一是 baseURL 设为 /api然后在开发环境通过 Vite 的 proxy 配置把请求转发到后端服务地址避免跨域问题二是响应拦截器统一判断后端的 code 字段而不是 HTTP 状态码。后端接口设计时建议统一返回格式{ code: 200, message: 操作成功, data: {} }这样做的好处是前端不用为每个接口单独处理错误逻辑。4.4 富文本编辑与 XSS 防护发布问题时内容编辑器是核心组件。wangEditor 是国内使用率较高的富文本编辑器轻量且功能全面。集成方式如下template Toolbar :editoreditorRef :defaultConfigtoolbarConfig / Editor v-modelcontent :defaultConfigeditorConfig onCreatedhandleCreated / /template script setup import wangeditor/editor/dist/css/style.css; import { Editor, Toolbar } from wangeditor/editor-for-vue; const content ref(); const editorRef shallowRef(); const toolbarConfig { excludeKeys: [group-image] }; const editorConfig { placeholder: 请输入问题描述..., MENU_CONF: { uploadImage: { server: /api/upload/image } } }; function handleCreated(editor) { editorRef.value editor; } /script关于 XSS 防护前端能做的有限核心还是要在后端做过滤。我建议在后端实现一个全局过滤器或者 AOP 切面对提交上来的富文本内容进行 HTML 标签白名单校验只保留 p、br、img、a 等安全标签过滤掉 script、iframe、link 等危险标签。这个思路与“全局过滤器处理上传 PDF 文件时 XSS 攻击”的需求类似本质是对用户输入做内容安全校验确保恶意代码不会被存储和渲染。5. 常见问题与排查技巧实录最后这部分是从实际开发中总结出来的高频问题排查清单。这些问题在课程答疑系统的开发过程中几乎都会遇到值得收藏备查。5.1 启动阶段问题排查问题一启动报错 Error creating bean with name dataSource这种问题的根源通常是数据库连接失败。先按以下顺序排查确认 MySQL 服务是否启动命令行执行mysql -u root -p是否能连接成功。排查密码是否正确注意 MySQL 8.0 默认使用了 caching_sha2_password 认证插件有些老版驱动不支持。确认连接 URL 中是否缺少 serverTimezone 参数。检查防火墙是否放行 3306 端口。如果本地 MySQL 连接报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock说明 MySQL 服务进程没有正常运行用systemctl status mysqld或service mysql status先查看服务状态再排查错误日志 /var/log/mysqld.log。问题二Invalid bound statement (not found)这个报错的含义是 Mapper 接口方法名和 XML 文件中的 id 不匹配。排查步骤检查 XML 文件的 namespace 是否和 Mapper 接口的全限定名一致。检查 XML 文件中的 id 是否和接口方法名一致。确认 application.yml 中的 mapper-locations 路径是否正确。确认 target 目录下是否真的编译出了 XML 文件有时 IDEA 不会自动 copy resources 目录下的 XML需要在 pom.xml 中配置 resources 包含路径。5.2 运行阶段问题排查问题三MyBatis 查询结果为 null 但 SQL 有数据这种情况通常是数据库字段和 Java 属性映射失败。如果你启用了 map-underscore-to-camel-case但数据库字段本身就是驼峰命名比如 createTime映射就有问题。建议数据库字段统一使用下划线命名Java 属性统一使用驼峰命名开启自动映射即可。另一个可能性是实体类没有无参构造方法或者属性没有被 Lombok 生成 getter/setter。问题四分页插件 PageHelper 不生效或者分页错乱PageHelper 使用时有几个硬性要求PageHelper.startPage() 后面必须紧跟第一条 SQL 查询语句中间不能有任何其他 SQL 操作只对紧接着的一条查询生效。如果在一个循环里多次使用 startPage会出现线程安全问题。推荐做法是在 Service 层调用 PageHelper.startPage然后在同一方法内直接调用 Mapper 查询。问题五前端请求跨域报错后端需要配置跨域支持Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(*) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意 allowCredentials(true) 不能和 allowedOrigins() 同时使用必须使用 allowedOriginPatterns()。这是 Spring 从 5.3 版本开始的安全限制。5.3 性能与安全配置建议答疑系统虽然体量不大但从一开始就应该养成一些习惯。首先是 MySQL 连接池。建议在配置中加入 HikariCP 的连接池参数这是 SpringBoot 默认的连接池实现spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 idle-timeout: 30000 connection-timeout: 30000 max-lifetime: 1800000连接池的作用是复用数据库连接避免每次请求都新建连接这在并发场景下能显著提升性能。其次是 MyBatis 缓存。一级缓存是 SqlSession 级别的默认开启同一个 SqlSession 内的相同查询会直接返回缓存结果。二级缓存是 Mapper 级别的需要在 XML 中配置cache/标签开启。但我建议答疑系统里不要轻易开启二级缓存——因为问题的点赞数和浏览量是高频更新的数据一旦缓存刷新不及时用户看到的数字就会不准确。我的建议是在查询问题详情时点赞数直接从数据库实时读取而不是走缓存保证数据的强一致性。最后是关于批量操作的优化。如果管理员需要批量删除问题或者批量修改问题的分类在 MyBatis 中推荐使用 foreach 标签构造 IN 查询或者使用 MyBatis 的批量执行器 BatchExecutorSqlSession sqlSession sqlSessionFactory.openSession(ExecutorType.BATCH); try { QuestionMapper mapper sqlSession.getMapper(QuestionMapper.class); for (Question q : questions) { mapper.insert(q); } sqlSession.commit(); } finally { sqlSession.close(); }批量模式下MyBatis 会把多条 SQL 放到同一个批次中提交网络往返次数从 N 次降为 1 次性能提升非常明显。我在实际项目中用这种方式批量导入课程数据时5000 条数据从之前逐条插入的 48 秒降到了 6 秒以内。6. 二次开发扩展方向与最终体会课程答疑系统做到基础功能可用之后后续的扩展方向其实很多。如果让我按优先级推荐我会建议从这几个方向入手第一个是接入消息推送服务。目前系统的站内信通知只能等用户登录后主动查看体验不够即时。可以集成 WebSocket在教师回答问题后实时推送给提问者。前端用 Vue 3 的 onMounted 生命周期建立 WebSocket 连接后端用 SpringBoot 的 WebSocket 支持实现成本不高但体验提升非常明显。第二个是增加内容审核机制。如果系统是公开运营的学生发布的内容必须经过审核才能展示否则就可能出现广告、辱骂甚至法律风险的内容。可以在问题表中增加 audit_status 字段0-待审核1-通过2-拒绝由管理员在后台审核。更进阶的方案是接入阿里云内容安全 API 做自动机审。如果项目不打算接入第三方服务也需要先做敏感词过滤的基础判断这是一条安全底线。第三个是数据可视化的增强。管理后台的统计页面可以增加趋势图每日提问量趋势、各课程分类提问占比饼图、教师回答效率排行榜这些用 ECharts 组件就能实现后端只需要提供对应的聚合查询接口。对于决策者来说这些图表比一堆原始数据有价值得多。第四个是学习资源的关联推荐。当学生搜索某个问题时系统可以自动匹配知识库文章或关联课程视频帮助学习者边问边学。这个功能在教育培训场景下尤其加分。作为实际做过在线教育系统的人我的体会是课程答疑系统的技术难度并不高它的核心价值在于业务逻辑的完整性和细节的把控——权限分得清不清楚、问题流转状态设计得合不合理、并发场景下的数据有没有一致性问题这些才是决定一个系统能不能真正用起来的因素。这套基于 SpringBoot Vue MyBatis MySQL 的源码技术栈足够主流、架构足够清晰、功能覆盖足够完整拿来学习或者二次开发都很合适。建议你拿到代码后做的第一件事不是急着跑起来看页面效果而是先把数据库建表脚本中的所有表关系画成一张 ER 图梳理清楚业务模块之间的关联这一步做完你后面读任何代码都会事半功倍。
返回列表