如果要做一个“国之动力科普网站”这样的内容展示平台用 JAVA 生态里的 SpringBoot3 处理后端接口用 Vue.js3 负责前端页面再用 MySQL 存放文章、栏目和管理员数据是一套很稳的基础组合。这个项目真正考验人的地方不是某个单独的框架有多难而是前后端分离之后从数据库设计、接口封装到页面联调到底该按什么顺序落地。我建议不要一开始就想着把页面做得特别炫也不要堆权限、商城、会员这类复杂模块。科普网站的核心链路只有两条一条是游客打开网站看内容、搜内容另一条是运营人员在后台上传内容、修改内容、管理分类。先把这两条主链路跑通剩下的功能都可以在现有骨架里慢慢加。下面按实际项目落地顺序拆一遍。内容偏工程实现代码部分给出关键示例你复现时再根据自己项目的包名和数据库名调整。1. 科普网站最容易被忽略的是先理清内容和角色“国之动力科普网站”的本质是一个以科普文章为主要载体的内容管理平台。和商城、管理系统不一样它没有复杂的交易流程也没有强权限组织架构。你要先决定“谁能看到什么”“谁能上传什么”“内容发布之后要不要审核”。1.1 游客端需要什么游客端最常见的动作就是浏览和搜索。首页要有栏目入口比如“能源装备”“动力原理”“科技人物”“历史上的一页”等。用户可以点进栏目看该栏目下按发布时间排列的文章列表也可以点开详情页阅读正文。除此之外搜索功能建议在第一次迭代就加上。科普网站内容多了以后用户通常心里带着一个具体的关键词例如“蒸汽轮机”“核能发电”“内燃机原理”如果只能按栏目找体验会比较差。这一侧的技术点不复杂但对数据结构的要求很明确栏目、文章、标签这些基础实体要能查得出来分页要稳定文章状态要及时过滤。也就是说游客端只管查询不做写入。1.2 管理端需要什么管理端不是给游客用的而是给后台运营人员维护内容用的。在一个典型科普项目中管理端至少要覆盖文章的新增、编辑、下线、删除栏目名称和排序的维护正文内容的富文本录入或者 Markdown 录入文章封面上传置顶、推荐、定时发布这类运营设置如果后续开了评论功能那么评论审核也必须进入管理后台。不用一开始就做细粒度权限比如“只有某某部门可以审核”“作者只能改自己文章”之类先做一个统一的 admin 管理角色就够了。1.3 先不要急着做用户注册和收藏用户注册、登录、收藏、点赞这些功能从扩展性上看很常规从迭代顺序上看却容易拖慢进度。我第一次做类似内容平台时把用户系统放到第一版结果真正花在内容展示和管理上的时间反而被压缩了。如果你不是专门做 B 端会员体系建议先只保留后台管理员账号游客侧全部开放访问。内容平台上线初期核心指标是有内容、能打开、搜得到。用户体系可以等流量起来后再做。判断标准很直接游客侧不需要登录就能阅读文章管理侧通过管理员账号进入后台维护。先守住这个边界项目不会失控。2. 技术选型怎么理解SpringBoot3、Vue3、MySQL 各负责什么很多人看到“JAVASpringBoot3Vue.js3MySQL”会觉得只是一个常规 CRUD 项目。确实如此但如果理解不到位后面会出现两种极端一种是谁都往上加新依赖最后系统启动一分钟另一种是只建了一个 Controller所有逻辑都堆在接口里维护成本很高。2.1 SpringBoot3 这层解决分层问题SpringBoot3 适合做接口层和业务层。按常规分层Controller 负责接收请求和返回结果Service 负责写业务Mapper 或 Repository 负责访问数据库。SpringBoot3 相比早期版本使用 Spring Framework 6默认基于 Java 17。本地如果还是 JDK 8大概率跑不起来。这不是说 JDK 8 不好而是 Boot3 的生态标尺已经不一样。复现这个项目时第一步先检查java -version再检查 Maven 配置避免把时间耗在环境报错上。2.2 Vue3 这层解决页面交互Vue3 负责页面渲染和数据交互。对于科普网站页面状态无非就是当前栏目、分页页码、搜索关键词、文章详情数据。Vue3 的组合式 API 用起来比原来的 Options API 更直接把请求逻辑和页面状态拆开也容易维护。管理端页面如果不想自己造轮子可以直接使用 Element Plus 这类现成组件库。表格、弹窗、表单校验、日期选择都能省不少开发量。不过组件库只是辅助不能代替你对业务字段的理解。2.3 MySQL 适合存结构化的内容元数据MySQL 是关系型数据库适合存放栏目表、文章表、标签表、评论表这些结构稳定的数据。正文内容如果很长可以存在text或longtext字段里配合utf8mb4字符集中文内容和常见符号都能正常保存。需要注意的是MySQL 虽然能存内容但不等于适合做复杂搜索。早期数据量不大时用LIKE %关键词%搜索一下没什么问题当文章到几千条以上这种搜索方式性能会明显下降。到那个阶段再考虑接入 Elasticsearch 或 MySQL 全文索引。2.4 为什么不建议一上来用微服务架构科普网站在第一版并不需要拆成多个服务。如果把文章服务、用户服务、搜索服务全部拆开反而要处理服务注册、配置中心、网关普通项目团队会被部署拖垮。单应用加前后端分离已经足够覆盖绝大多数展示型网站。真正要做的是把代码切面做好前端分成用户端和管理端后端按模块分包。以后即使从一个单体应用拆出去改造路径也清晰。3. 数据库设计一张文章表先要能表达清楚发布业务数据表是整个项目的地基。页面样式可以随时改接口可以反复调但数据库结构一旦上线后再改很多地方都要跟着动。科普网站虽然实体不多我还是建议先画一画关系。3.1 基础表结构可以这样规划第一版建议建这几张表表名用途核心字段方向category栏目id、名称、排序号、父级idarticle文章id、标题、栏目id、作者、封面、正文、状态tag标签id、名称article_tag文章标签关系article_id、tag_idcomment评论id、文章id、昵称、内容、审核状态admin_user管理员id、用户名、加密密码、角色一开始想不清楚没关系但文章和栏目必须拆表。因为一篇文章只能归一个主栏目同时还要支持多标签。如果文章和栏目的关系放在一个字段里用逗号拼接后面统计和筛选会非常难受。3.2 文章表字段怎么定义文章表的核心字段大概是这样的CREATE TABLE article ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, category_id BIGINT NOT NULL, author VARCHAR(100) DEFAULT , summary VARCHAR(500) DEFAULT , cover_url VARCHAR(500) DEFAULT , content MEDIUMTEXT, status TINYINT DEFAULT 0 COMMENT 0草稿 1已发布 2下线, is_top TINYINT DEFAULT 0 COMMENT 0不置顶 1置顶, view_count INT DEFAULT 0, published_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_category_status (category_id, status), KEY idx_published_at (published_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status字段在游客端列表查询里必须参与过滤。你不能把草稿也展示出去。published_at是文章真正发布时间不一定要和创建时间一样。之所以把view_count也放进去是为了做“热门文章”这类展示。虽然直接每次UPDATE article SET view_count view_count 1会有一些性能问题但初期这个写法最简单实用。真要扛高并发再引入 Redis。3.3 评论要不要审核科普网站如果开放评论就一定会有低质量内容和广告。第一版建议把评论status也设计成 0 待审核、1 通过、2 拒绝。这样游客即使提交了评论也要在管理端审核之后才展示。还要给article_id建普通索引否则用户浏览一篇热门文章时数据库要根据文章 id 去评论表里筛选每次都全表扫描就不合适了。3.4 数据库初始化用脚本不要手动点表创建数据库时先用一个init.sql把建表语句管理起来。这样好处很多新同事 clone 项目后直接执行一条命令就能建表上线环境可以复用同一份结构后续结构升级可以写增量脚本不要在本地手工改完数据库以后忘记同步到脚本里。否则换一台机器时项目启动看到的表和开发环境完全不一样排查成本会很高。4. 后端接口怎么搭先把一条主链路跑通再说后端是前后端分离中间的桥梁。我不建议把全部表对应的增删改查一次性写完那会让你陷入大量重复代码。更合理的顺序是先写好文章列表接口再写文章详情接口验证一套流程能通再补管理端接口。4.1 SpringBoot3 项目的基本结构一个典型项目结构可以长这样src/main/java/com/example/statepower ├── config // 跨域、拦截器、上传配置 ├── controller // 接口入口 ├── service // 业务逻辑 ├── mapper // 数据库访问 ├── entity // 实体类 ├── dto // 接口参数和返回对象 └── common // 统一返回结构、异常处理实体类里的字段和数据库字段对应。为了省代码可以使用 Lombok 的Data注解。返回结构建议统一用一个ResultT包装里面包含 code、message、data 三个字段。不要一个接口返回一个样前端联调会非常痛苦。4.2 最小依赖和配置示例如果你用 Maven依赖大致包含dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- 数据库连接 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency示例里不写具体的依赖版本建议用 Spring Initializr 选择一个稳定的 SpringBoot3 版本再由 Maven 统一管理。application.yml的核心配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/state_power?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverJDBC URL 里的characterEncoding和字符集设置必须和数据库保持一致否则文章里保存特殊符号时可能出问题。4.3 用户端接口先做文章列表和详情前面提到表结构有status和published_at那么列表接口必须默认只查“已发布”的文章。一个简单的 Controller 示例RestController RequestMapping(/api/public/article) public class ArticleController { Autowired private ArticleService articleService; GetMapping(/list) public ResultPageResultArticleVO list( RequestParam(required false) Long categoryId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return Result.ok(articleService.queryPublished(categoryId, page, size)); } GetMapping(/{id}) public ResultArticleVO detail(PathVariable Long id) { return Result.ok(articleService.getPublishedById(id)); } }在 Service 里要注意两点。第一详情查询也必须带status 1条件防止后台人员把未发布的文章链接发给别人后别人也能通过 id 手动访问到。第二列表的排序先用“是否置顶”和“发布时间倒序”把最新已发布的内容放前面。4.4 管理端接口不要裸奔管理端接口要区别于用户端接口。常见做法是请求/api/admin/article时先通过拦截器校验管理员 token。第一次迭代不建议自己写完整权限框架但“别人知道接口也不应该随便删内容”这个安全问题必须考虑。可以简单用 JWT 生成一个管理员登录凭证拦截器里校验请求头中的 token然后放行到对应管理接口。实现逻辑一般是这样管理员提交用户名和密码后端验证密码验证通过后生成 token前端把 token 存到本地后续管理端请求在 header 里带上 token写这部分时密码存储不要用明文。建议用 BCrypt 这类哈希算法生成不可逆的密码串即使数据库泄露攻击者也无法直接拿到原始密码。4.5 统一异常处理别忽略很多新手项目只在 Controller 里返回成功数据出错时直接抛异常让前端看到一大段错误堆栈。这样既不友好也不适合判断问题。在 SpringBoot3 里加一个RestControllerAdvice全局异常处理类把业务异常、参数校验异常、兜底异常分别处理。前端拿到统一结构后弹错误提示也很方便。比如使用乐观估计的规则如果接口逻辑里发现参数异常抛出业务异常全局异常处理器返回code 400如果出现系统错误返回code 500。前端只需要根据 code 判断成功或失败不用去解析异常堆栈。5. Vue3 页面怎么接前端不是孤立的静态页Vue3 项目常见的初始化方式是用 Vite 脚手架。你可以创建项目后再逐步页面向导修改。这类科普网站页面不多但每个页面的数据都依赖后端接口。5.1 创建 Vue3 工程和后端端口匹配前端项目创建后建议在根目录建一个.env.development文件配置接口地址VITE_API_BASE_URL/api开发阶段使用 Vite 代理把/api转发到后端地址// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })用代理的方式而不是直接写http://localhost:8080的原因是避免浏览器跨域。前端页面跑在 5173后端跑在 8080二者协议、域名、端口都不同浏览器默认会拦截非同源请求。5.2 页面路由和页面拆分科普网站前端页面建议分两套目录src/views ├── home │ └── HomeView.vue ├── article │ ├── ArticleList.vue │ └── ArticleDetail.vue └── admin ├── Login.vue ├── Layout.vue ├── ArticleManage.vue └── CategoryManage.vue用户端路由正常配置/ 首页 /great-power?catxx 列表页 /great-power/:id 详情页管理端路由建议单独加一个/admin前缀路由进入之前做一次管理员登录状态判断。如果没登录直接跳转登录页。5.3 列表页如何渲染后端数据列表页逻辑上要做三件事读取路由参数中的 categoryId通过请求获取文章列表把返回数据渲染到页面。常用的组合式写法const route useRoute() const page ref(1) const list ref([]) const loading ref(false) async function loadList() { loading.value true try { const res await request.get(/public/article/list, { params: { categoryId: route.query.cat || undefined, page: page.value, size: 10 } }) list.value res.data.records } finally { loading.value false } }这里每次切换栏目时要记得把 page 重置成 1。不然用户在第一页看到一半又切到另一个栏目结果还在当前页码很可能返回空数据。5.4 管理后台的表格页面管理后台通常是 Element Plus 的表格配合弹窗表单。后端返回的文章列表字段可以直接映射到表格列标题、所属栏目、发布时间、状态、操作按钮。使用表格前先确认后端返回结构里分页字段的命名。不同后端返回的分页格式不一样有的接口返回records有的返回rows。前端 axios 请求封装时最好先约定好免得每个页面单独调整。编辑文章时封面图建议先传到后端再用接口返回的访问地址作为coverUrl字段保存。不要直接前端把一张图片转成 base64 塞到数据库里否则文章一多数据库会变得非常大加载也会卡。6. 本地联调最容易踩的坑按顺序排一遍前后端分离项目最耗时的往往是“两边都已经写完但接不起来”。下面这些坑我在项目里反复遇到过尤其适合新手提前避雷。6.1 启动顺序先数据库再后端再前端正确顺序是先把 MySQL 服务起来确认可以连接再启动 SpringBoot3 后端最后启动 Vue 开发服务器。如果第一时间就报连接失败优先确认 MySQL 是否在运行而不是去改前端代码。后端启动时如果日志一直停在连接数据源阶段多半是数据库 URL 不对、用户名密码不对或者 MySQL 端口被占用。先把这些前置项确认清楚能省很多时间。6.2 端口冲突和跨域SpringBoot3 默认监听 8080Vite 默认监听 5173。如果本地 8080 端口已经被占用后端会启动失败。可以在 application.yml 里改成其他端口。跨域问题常见的表现是浏览器 F12 控制台里有类似Access to XMLHttpRequest ... CORS的报错但其实后端接口能通。开发阶段优先让前端走 Vite 代理线上环境用 Nginx 把/api反向代理到 SpringBoot 服务不建议靠后端开启全局 CORS 来解决生产环境问题那样配置不好容易把接口暴露给任意来源。6.3 数据库中文乱码或内容截断出现中文乱码时检查三个位置MySQL 数据库、表、字段的字符集是否是 utf8mb4JDBC URL 是否带了characterEncodingutf8页面meta charset和 HTTP 响应头是否正常如果保存长篇科普内容时提示字段长度不够建议用MEDIUMTEXT或LONGTEXT。普通VARCHAR(255)不适合装几千字正文。6.4 图片上传和访问路径科普文章的封面、正文里的配图都是资源文件。最稳妥的做法是把上传目录放在服务器的一个固定目录下比如/var/data/statepower/images后端上传接口保存完文件后返回给前端一个能访问的静态路径。SpringBoot 里可以通过ResourceHandler把磁盘目录映射成访问路径。但要注意部署到云服务器后不要硬编码本地路径到代码里。建议在配置文件里用可配置的upload-dir不同环境设置不同路径。6.5 管理员 token 在刷新页面后失效前端管理后台用 localStorage 保存 token刷新页面后仍然需要判断 token 是否过期。比较简单的做法是在 axios 响应的拦截器里判断返回状态码如果出现 401 未登录就跳转到登录页并清空本地用户信息。部分项目会部署在 Nginx 下管理端路由刷新后出现 404。这是因为 Vue 是单页应用刷新/admin/article时 Nginx 不知道它应该交给前端。需要在 Nginx server 配置里加类似try_files $uri $uri/ /index.html;把不存在的路径归到前端入口文件。7. 真正上线前建议再补这几项优化第一版能跑通绝不等于能放心上线。科普网站属于内容型站点拼的是稳定性和长期维护能力。我在实际运维中遇到过不少“本地没问题上线后被打挂”的情况所以把下面这些优化放在最后。7.1 内容搜索的长期处理先使用 MySQL 的LIKE查询做站点搜索当文章数量增长后你会发现搜索一个关键词可能要扫很多条记录。常见的优化路径是先保证查询走索引再考虑 MySQL 内置全文索引最后再评估 Elasticsearch第一版运营时如果内容是几百篇完全不需要上重量级搜索组件。优先保证接口稳定别让搜索拖垮数据库。7.2 加上 Redis 缓存热门栏目和热点文章科普网站首页通常会被频繁访问。首页栏目和推荐文章变化频率低适合缓存。SpringBoot3 项目里加入 Redis 后可以把栏目列表、热门文章列表缓存起来。容易踩的坑是缓存后修改文章但首页马上不生效。解决办法很简单写一个简单的缓存过期策略比如“文章保存成功后删除对应缓存 key”下次请求再重新加载。7.3 日志、备份和自动化部署上线前要把后端日志按天切割数据库建议每天自动备份前端打出静态文件后由 Nginx 托管。如果这些不做后续一旦出问题恢复数据会非常痛苦。数据库备份其实一句话就能理解备份的是数据不是代码。代码丢了可以重新构建文章内容丢了就很难找回。科普网站运营得越久文章数据越宝贵。7.4 访问速度和安全加固部署时静态资源尽量用域名加 CDN 加速。后端接口不能暴露到公网以后就不管了至少需要修改默认 root 密码不开放不必要的数据库端口管理端接口必须走登录验证对上传文件做类型和大小限制内容型网站最容易受到的是垃圾评论、上传恶意文件、后台弱口令这类问题。这些不需要高深安全技术只要在代码里增加校验和限制就能规避大部分常见风险。这个项目真正落到生产环境时最值得盯住的不是哪一项技术有多新而是文章状态是否过滤干净、管理接口是否加验证、数据库字符集是否正确、上传路径是否可控。先把单条内容链路的查询跑稳定再考虑做更复杂的推荐、聚合和搜索。科普内容本身是一个会持续生长的数据库后台维护的顺手程度往往比首页一时的视觉效果更能决定项目能否长期用下去。
