ARTICLE DETAIL

资讯详情

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

SpringBoot3+Vue3+MySQL:科普网站前后端分离实战解析

SpringBoot3+Vue3+MySQL:科普网站前后端分离实战解析 这次我们来看一个非常典型的 Java 全栈实战项目方向国之动力科普网站。它不是那种包装复杂的低代码平台而是用目前企业里最主流的组合JAVA SpringBoot3 Vue.js3 MySQL搭出来的前后端分离科普内容管理系统。无论你是准备做毕业设计还是想自己练一个能写进简历的完整 Web 项目这套技术栈都足够有代表性。项目的核心问题很简单科普内容要能发布、能分类、能检索、能管理并且页面要能稳定展示。所以它的重点不在 AI、不在大模型而在后端接口设计、数据库建模、前端页面渲染、登录鉴权和批量内容管理这些后端开发基本功上。这篇文章会直接用一套可落地的工程结构把这个类型网站从数据库设计、后端接口、前端页面到本地启动全部走一遍并给出实际可复制的代码骨架和验证流程。本文适合下面几类读者正在学 SpringBoot3 的 Java 后端、准备做 Vue3 管理后台或内容站的前端、需要一套科普类 CMS 参考实现的开发者。读完至少能获得三个收益第一知道这类项目应该如何拆模块、建表、写接口第二拿到一套能启动的前后端最小工程结构第三理解上线前要做哪些配置、测试和排查。1. 项目定位与核心能力速览先把整个项目的能力边界画出来。从项目名称看网站的主题是“科普”核心功能路径一般是普通访客浏览科普文章/专题 → 按分类检索内容 → 管理员登录后台 → 对文章进行新增、编辑、上下架、删除 → 内容在前台同步展示。能力项说明技术栈Java、SpringBoot3、Vue.js3、MySQL项目类型前后端分离的科普内容展示与管理网站主要功能科普文章列表、文章详情、分类浏览、搜索、后台登录、文章管理、分类管理、批量上下架前端用户普通访客不登录可浏览、内容管理员后台维护后端服务RESTful API、统一返回结构、JWT 登录鉴权、参数校验数据存储MySQL 存储用户、分类、文章、标签等核心数据启动方式本地启动后端 SpringBoot 服务 前端 Vue3 开发服务器部署形态前后端可分离部署也可打包后由 Nginx 统一托管适合场景Java 全栈练手、毕业设计、科普内容站快速原型需要提醒一个点由于原始项目介绍比较精简这里不会写死某个开源仓库的细节。更多是把该技术栈下“科普网站”这类项目最通用、最可靠的实现路径讲完整你在实际复刻时把表名、字段、页面文案替换成自己的内容即可。2. 科普网站功能设计与使用边界这类网站要做的不是花哨页面而是把一套“内容从录入到发布”的链路走通畅。开发前先把角色和核心流程说清楚写代码时才不会乱。2.1 访客侧与管理员侧功能划分普通访客能做的事情相对简单浏览首页推荐的科普文章。按科普分类查看列表比如“能源动力”“科技前沿”“工程原理”等。通过标题关键字搜索文章。查看文章详情包括正文、发布时间、来源、所属分类。管理员侧才是系统重点通常需要拆成独立的后台界面管理员登录和退出登录。分类管理新增、重命名、停用分类。文章管理新增文章、编辑文章、删除文章、批量上下架。内容审核状态控制草稿、已发布、已下架。网站基础统计文章总数、分类数量、最近发布数量。2.2 核心业务规则设计时要明确几点规则避免后面功能改来改去文章状态建议用枚举值0 草稿、1 已发布、2 已下架。前台只展示“已发布”的文章。修改文章后如果原来是已发布状态可以选择重新发布或者转为草稿。分类逻辑上不建议做太深当前项目一级分类即可如果未来扩展二级分类需要再加 parent_id 字段。科普文章内容可能较长正文建议使用TEXT或LONGTEXT类型存储富文本或 Markdown 内容可在前端自行选择渲染方案。2.3 使用边界与合规提醒科普网站内容面向公众需要注意几个边界也同样适用于任何内容发布系统内容准确性问题涉及科技、工程、能源等内容时发布前要有来源核对机制避免传播未经证实的信息。版权问题转载科普文章要获得授权图片、视频、字体素材要确认版权。内容安全后台需要登录鉴权不能让未登录用户直接调用管理接口公开接口需要增加访问频率限制防止被刷。个人隐私系统内如果有用户注册、评论等功能涉及用户昵称、邮箱、手机号时应脱敏展示并遵守隐私保护要求。2.4 开发时需要提前想清楚的非功能需求除了功能之外下面的问题如果不提前设计后期会很被动文章列表接口是否分页必须分页不然数据一多页面卡顿。文章正文是否支持图片上传如果需要要提前规划上传接口和静态资源访问路径。是否需要 Markdown 编辑器如果希望编辑体验好可以在后台引入 Markdown 编辑器对应的前端需要有渲染组件。是否记录文章创建人、创建时间、更新时间建议统一增加 create_time、update_time 字段。管理员数量多不多如果只有一个管理员登录表可以简单一些如果多人管理建议加角色字段。3. 技术栈拆分与数据库设计SpringBoot3 对 JDK 版本有要求一般建议使用JDK 17 及以上。前端 Vue.js3 配合 Vite 构建开发效率比 Webpack 时代高很多。MySQL 建议使用8.0 及以上方便使用更完善的 JSON 支持和字符集能力。3.1 技术栈与工具清单后端 - JDK 17 - SpringBoot 3.x - Spring Web - Spring Data JPA 或 MyBatis-Plus - Spring Validation - MySQL Connector/J - JWT登录鉴权可使用 jjwt 或 hutool 等工具库 前端 - Node.js 18 - Vue 3 - Vite - Vue Router 4 - Pinia - Axios - Element Plus后台管理界面组件库 数据库 - MySQL 8.0 - Navicat 或 MySQL Workbench如果团队习惯 MyBatis也可以把持久层换成MyBatis-Plus它在单表 CRUD 场景下非常省事。下面示例使用 Spring Data JPA 思路做讲解实际项目二选一即可。3.2 数据库表设计科普网站最核心的就是文章表和分类表。这里提供一套最简但可运行的表结构3.2.1 分类表 categoryCREATE TABLE category ( id bigint NOT NULL AUTO_INCREMENT COMMENT 分类ID, name varchar(64) NOT NULL COMMENT 分类名称, sort int DEFAULT 0 COMMENT 排序号越小越靠前, status tinyint DEFAULT 1 COMMENT 状态1启用 0停用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT科普分类表;3.2.2 文章表 articleCREATE TABLE article ( id bigint NOT NULL AUTO_INCREMENT COMMENT 文章ID, category_id bigint NOT NULL COMMENT 分类ID, title varchar(200) NOT NULL COMMENT 文章标题, summary varchar(500) DEFAULT NULL COMMENT 摘要, cover varchar(500) DEFAULT NULL COMMENT 封面图URL, content longtext COMMENT 正文内容, source varchar(100) DEFAULT NULL COMMENT 内容来源, author varchar(64) DEFAULT NULL COMMENT 作者, status tinyint DEFAULT 0 COMMENT 状态0草稿 1已发布 2已下架, view_count bigint DEFAULT 0 COMMENT 浏览量, publish_time datetime DEFAULT NULL COMMENT 发布时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category_status (category_id, status), KEY idx_publish_time (publish_time), FULLTEXT KEY ft_title_content (title, content) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT科普文章表;3.2.3 管理员表 admin_userCREATE TABLE admin_user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 管理员ID, username varchar(64) NOT NULL COMMENT 登录名, password varchar(128) NOT NULL COMMENT 密码BCrypt加密, nickname varchar(64) DEFAULT NULL COMMENT 昵称, status tinyint DEFAULT 1 COMMENT 状态1启用 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT管理员表;从设计角度看三张表就足够支撑一个最小可用的科普网站。如果后续要扩展标签、评论、友情链接再单独加表就行。4. 后端 SpringBoot3 工程结构与关键代码后端工程建议按照常见的分层结构组织避免所有代码堆在一个类里。src/main/java/com/example/techpop ├── TechPopApplication.java ├── common │ ├── Result.java │ ├── ResultCode.java │ └── exception │ ├── BusinessException.java │ └── GlobalExceptionHandler.java ├── config │ ├── CorsConfig.java │ └── WebMvcConfig.java ├── controller │ ├── AdminArticleController.java │ ├── AdminAuthController.java │ ├── AdminCategoryController.java │ └── PortalArticleController.java ├── entity │ ├── Article.java │ ├── Category.java │ └── AdminUser.java ├── repository │ ├── ArticleRepository.java │ ├── CategoryRepository.java │ └── AdminUserRepository.java ├── service │ ├── ArticleService.java │ ├── CategoryService.java │ └── AdminUserService.java ├── dto │ ├── ArticleQuery.java │ ├── ArticleSaveRequest.java │ └── LoginRequest.java └── util └── JwtUtil.java4.1 Maven 依赖示例以下是核心的 pom.xml 文件内容节选启动类按 SpringBoot3 常规写法即可。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.4/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies4.2 统一返回结果 Result 类接口层不要直接返回 Map 或裸对象最好定义统一的 Result 包装类前端处理起来更省事。package com.example.techpop.common; public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } // getter / setter 省略实际代码需要补全 }4.3 application.yml 配置配置文件中需要重点确认 URL、用户名、密码和 JPA 的 ddl-auto 策略。生产环境建议把 ddl-auto 设为 validate 或 none避免自动改表结构。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/tech_pop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true app: jwt: # 生产环境不要使用硬编码默认值建议通过环境变量注入 secret: your-secret-key-change-in-production expire-hours: 244.4 JWT 登录与鉴权实现登录流程不复杂前端提交 username 和 password后端用 BCrypt 校验密码通过后生成 JWT 返回给前端前端把 token 存起来后续请求放到 Authorization header 中。package com.example.techpop.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; Component public class JwtUtil { Value(${app.jwt.secret}) private String secret; Value(${app.jwt.expire-hours}) private Long expireHours; private SecretKey getSecretKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(Long userId, String username) { Date now new Date(); Date expireDate new Date(now.getTime() expireHours * 3600 * 1000); return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expireDate) .signWith(getSecretKey()) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getSecretKey()) .build() .parseClaimsJws(token) .getBody(); } }4.5 文章查询接口示例前台列表接口带分页、分类筛选和关键字搜索。使用 JPA 时可以直接写 Specification 或命名方法查询下面示例用 JPA 方法名方式做简单查询。package com.example.techpop.repository; import com.example.techpop.entity.Article; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface ArticleRepository extends JpaRepositoryArticle, Long { PageArticle findByStatusAndCategoryIdAndTitleContaining( Integer status, Long categoryId, String keyword, Pageable pageable); PageArticle findByStatusAndCategoryId(Integer status, Long categoryId, Pageable pageable); PageArticle findByStatus(Integer status, Pageable pageable); }控制层写法如下package com.example.techpop.controller; import com.example.techpop.common.Result; import com.example.techpop.dto.ArticleQuery; import com.example.techpop.entity.Article; import com.example.techpop.service.PortalArticleService; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/portal/articles) public class PortalArticleController { private final PortalArticleService portalArticleService; public PortalArticleController(PortalArticleService portalArticleService) { this.portalArticleService portalArticleService; } GetMapping public ResultPageArticle list(ArticleQuery query) { return Result.success(portalArticleService.listPublished(query)); } GetMapping(/{id}) public ResultArticle detail(PathVariable Long id) { return portalArticleService.getPublishedDetail(id); } }ArticleQuery 里包含 page、size、categoryId、keyword由 Spring MVC 自动绑定 GET 参数。ArticleService里需要先查是否存在再决定抛异常还是返回数据。例如前台查看详情时如果文章不存在或状态不是已发布直接返回业务错误。5. 前端 Vue.js3 工程搭建前端部分推荐直接用 Vite 创建 Vue3 工程安装路由、状态管理和 UI 组件库。npm create vitelatest techpop-web -- --template vue cd techpop-web npm install npm install vue-router4 pinia axios element-plus5.1 前台页面目录src ├── api │ ├── portal.js │ └── admin.js ├── router │ └── index.js ├── stores │ └── user.js ├── views │ ├── portal │ │ ├── HomeView.vue │ │ ├── CategoryView.vue │ │ └── ArticleDetailView.vue │ └── admin │ ├── AdminLoginView.vue │ ├── AdminLayout.vue │ ├── ArticleListView.vue │ ├── ArticleEditView.vue │ └── CategoryManageView.vue └── main.js前台和后台建议拆成两个布局。前台走访问者视角后台走管理视角。比如路由可以分成/和/admin两套。import { createRouter, createWebHistory } from vue-router; const routes [ { path: /, component: () import(../views/portal/HomeView.vue), meta: { title: 首页 } }, { path: /category/:id, component: () import(../views/portal/CategoryView.vue), meta: { title: 分类科普 } }, { path: /article/:id, component: () import(../views/portal/ArticleDetailView.vue), meta: { title: 文章详情 } }, { path: /admin/login, component: () import(../views/admin/AdminLoginView.vue) }, { path: /admin, component: () import(../views/admin/AdminLayout.vue), meta: { requiresAuth: true }, children: [ { path: , redirect: /admin/articles }, { path: articles, component: () import(../views/admin/ArticleListView.vue) }, { path: articles/edit/:id?, component: () import(../views/admin/ArticleEditView.vue) }, { path: categories, component: () import(../views/admin/CategoryManageView.vue) } ] } ]; const router createRouter({ history: createWebHistory(), routes }); router.beforeEach((to, from, next) { const token localStorage.getItem(admin_token); if (to.meta.requiresAuth !token) { next(/admin/login); } else { next(); } }); export default router;5.2 Axios 请求封装请求封装要统一处理 token 注入和响应异常特别是 401 跳转登录。import axios from axios; import { ElMessage } from element-plus; import router from ../router; const request axios.create({ baseURL: http://localhost:8080/api, timeout: 15000 }); request.interceptors.request.use( (config) { const token localStorage.getItem(admin_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) Promise.reject(error) ); 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.data; }, (error) { if (error.response error.response.status 401) { localStorage.removeItem(admin_token); router.push(/admin/login); } ElMessage.error(error.response?.data?.message || 网络异常); return Promise.reject(error); } ); export default request;5.3 前台文章列表页面以一个简单的科普列表页为例页面加载时调用后端接口把返回值渲染成卡片列表。script setup import { ref, onMounted } from vue; import { useRoute, useRouter } from vue-router; import { getArticleList } from ../../api/portal; const route useRoute(); const router useRouter(); const articles ref([]); const total ref(0); const loading ref(false); const page ref(1); const size ref(10); async function loadArticles() { loading.value true; try { const categoryId route.params.categoryId || ; const data await getArticleList({ page: page.value - 1, size: size.value, categoryId, keyword: route.query.keyword }); articles.value data.content; total.value data.totalElements; } finally { loading.value false; } } function goDetail(id) { router.push(/article/${id}); } onMounted(loadArticles); /script template div h2科普文章/h2 div v-loadingloading div v-forarticle in articles :keyarticle.id classarticle-card clickgoDetail(article.id) h3{{ article.title }}/h3 p{{ article.summary }}/p span{{ article.author }}/span span{{ article.publishTime }}/span /div /div /div /template前端开发时最常见的联调问题就是跨域。后端需要配置跨域访问开发模式下也可以在前端 Vite 配置 proxy 把/api代理到后端端口。5.4 后台文章管理列表管理列表会多一些操作按钮比如编辑、删除、上下架。建议用 Element Plus 的 Table 组件配合分页组件接口复用后端POST /api/admin/articles/query这类管理端分页查询接口。管理端查询和前端展示查询要分开因为管理端要能看到草稿和下架的记录而前台只展示已发布数据。6. 本地启动与前后端联调先说结论整个项目启动流程可以归纳为四步。创建数据库并执行建表 SQL。启动后端 SpringBoot 服务。启动前端 Vite 开发服务器。浏览器访问前端地址验证接口连通性。6.1 创建数据库mysql -uroot -p进入 MySQL 后执行CREATE DATABASE tech_pop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE tech_pop;然后执行本章第 3 节中的建表 SQL再插入一条分类和文章测试数据。INSERT INTO category (name, sort, status) VALUES (能源动力, 1, 1); INSERT INTO article (category_id, title, summary, content, status, publish_time) VALUES ( 1, 什么是燃煤发电, 简要介绍燃煤发电的基本原理和能量转化过程, 燃煤发电的基本原理是将燃料中的化学能转化为热能再利用热能产生高温高压蒸汽推动汽轮机转动最终带动发电机产生电能。, 1, NOW() );6.2 启动后端服务在 IDEA 里直接运行 TechPopApplication或者使用 Maven 命令行mvn spring-boot:run启动成功后终端能看到 Tomcat started on port 8080说明后端服务已经可访问。此时访问http://localhost:8080/api/portal/articles如果返回 JSON 数据后端就正常。6.3 启动前端开发服务在 techpop-web 目录执行npm install npm run dev终端会输出 Vite 的访问地址默认是http://localhost:5173。6.4 联调验证清单联调阶段建议按下面顺序验证不要跳步骤访客直接访问首页确认能加载文章列表。点击分类确认列表按分类过滤。点击文章确认正文详情能正常展示。未登录时访问后台地址应被路由守卫重定向到登录页。用初始化好的管理员账号登录确认能获取 token。登录后进入文章管理页能查看所有状态的文章。新增一篇草稿文章在前台应该看不到发布后前台才能看到。批量勾选文章下架确认前台列表同步变化。其中第 5 步前端页面需要把后端返回的 token 保存到 localStorage具体字段名要前后端协商一致。下面给一个登录按钮的调用示例script setup import { reactive, ref } from vue; import { useRouter } from vue-router; import { doLogin } from ../../api/admin; import { ElMessage } from element-plus; const router useRouter(); const loading ref(false); const form reactive({ username: , password: }); async function handleLogin() { if (!form.username || !form.password) { ElMessage.warning(请输入用户名和密码); return; } loading.value true; try { const data await doLogin(form); localStorage.setItem(admin_token, data.token); localStorage.setItem(admin_name, data.nickname || form.username); router.push(/admin/articles); } finally { loading.value false; } } /script注意上面 axios 封装里已经做过响应拦截返回结果是res.data所以data.token直接拿的就是后端返回体里的 token 字段。7. 核心接口设计与批量任务场景科普网站虽然内容偏展示但只要后续维护工作量上来接口设计就必须考虑通用性和批处理能力。7.1 统一接口返回结构建议所有接口统一返回{ code: 200, message: 操作成功, data: {} }前端只用判断 code 是否为 200就能决定是否继续渲染逻辑。7.2 核心接口清单模块方法路径说明是否需要登录前台GET/api/portal/articles分页查询已发布文章否前台GET/api/portal/articles/{id}查询已发布文章详情否前台GET/api/portal/categories查询启用分类否认证POST/api/admin/auth/login管理员登录否后台GET/api/admin/articles分页查询全部文章是后台POST/api/admin/articles新增文章是后台PUT/api/admin/articles/{id}编辑文章是后台PUT/api/admin/articles/{id}/status修改文章状态是后台DELETE/api/admin/articles/{id}删除文章是后台POST/api/admin/articles/batch-status批量上下架是批量接口建议设计成下面这样{ ids: [1, 2, 3], status: 2 }后端 Service 层用一个循环或一条 update 完成即可。如果处理上百条数据建议使用 JPA 的findAllById查出实体列表再批量设置状态后调用saveAll这样可以减少多次数据库往返的隐患。package com.example.techpop.service; import com.example.techpop.entity.Article; import com.example.techpop.repository.ArticleRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; Service public class ArticleBatchService { private final ArticleRepository articleRepository; public ArticleBatchService(ArticleRepository articleRepository) { this.articleRepository articleRepository; } Transactional public void batchUpdateStatus(ListLong ids, Integer status) { ListArticle articles articleRepository.findAllById(ids); for (Article article : articles) { article.setStatus(status); if (status 1) { article.setPublishTime(java.time.LocalDateTime.now()); } } articleRepository.saveAll(articles); } }7.3 管理端文章导入批量任务如果需要把 Excel 或 CSV 里的科普文章批量导入到系统应该单独设计一个导入任务而不是通过后台表单逐条提交。比较稳妥的做法是前端上传 CSV 文件到后端。后端解析文件校验必填字段标题、分类名称、摘要、正文。按分类名称匹配已有分类不存在则自动创建或忽略。将合法数据批量保存非法数据记录到失败列表。返回成功导入数量和失败原因前端下载失败记录。这一套流程复杂度不高但能极大提升后台内容维护效率。批量导入时的关键词提示如果 CSV 中包含无分类的记录建议跳过而不是直接报错整个任务失败这样更符合生产环境实际需求。7.4 API 调用示例开发者调试时可以先用 curl 验证接口。以后台登录接口为例curl -X POST http://localhost:8080/api/admin/auth/login \ -H Content-Type: application/json \ -d { username: admin, password: 123456 }正常返回示例{ code: 200, message: 操作成功, data: { token: eyJhbGciOiJIUzI1NiJ9.xxx.xxx, nickname: 系统管理员 } }调用需要鉴权的后台接口时把 token 放到请求头curl -X GET http://localhost:8080/api/admin/articles?page0size10 \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx8. 批量任务与性能观察方法科普网站的接口没有大模型推理那么重的资源消耗但也不代表不用关注性能。前后端分离架构下的常见性能瓶颈主要集中在三个方面慢查询、大字段传输、前端渲染数量过大。8.1 后端常见瓶颈文章列表接口如果使用SELECT *并且把所有记录的 content 大字段返回会非常浪费带宽。管理端列表应该只查 id、title、summary、status、create_time、publish_time不查 content。等编辑时再按 id 查详情。对于全文搜索不要直接用LIKE %关键词%去查 content 字段数据量大时性能很差。可以先用标题模糊搜索后续需要全文搜索再引入 Elasticsearch。列表接口一定要做分页前端传 page 和 size后端限制最大 size 不要超过 50防止有人一次性拉全表数据。8.2 前端性能观察文章列表图片不要直接加载原图前端可以使用缩略图后端返回 cover 字段时可以用同一个 URL但上传时最好生成多尺寸图片。首页推荐接口如果有轮播图请不要一次性请求所有图片资源使用懒加载。如果后台文章列表上千条不要在 Table 中一次性渲染全部数据必须使用分页组件每页 10 到 20 条比较合适。8.3 如何观察服务占用的资源后端启动后可以在终端通过 JDK 自带命令查看进程状态jps -l如果要看后端进程的 CPU 和内存top -p pid如果是 Windows 环境可以直接打开任务管理器查看 Java 进程占用前端node进程对应 Vite 开发服务器。这个项目在开发模式下后端占用内存通常在 300MB 到 800MB 之间差异取决于 IDE、JVM 参数和依赖数量前端开发服务器占用相对较小。8.4 启动参数的调优建议如果本机内存有限启动后端时可以限制 JVM 堆内存java -Xms256m -Xmx512m -jar techpop-web.jar多环境配置可以拆成application-dev.yml、application-prod.yml通过启动参数指定环境java -jar techpop-web.jar --spring.profiles.activedev这样的好处是数据库连接、JWT 密钥、日志级别可以分开管理不至于把生产信息暴露到开发配置中。9. 常见问题与排查方法下面整理一套从开发到部署最常见的排错清单按现象分类。问题现象可能原因排查方式解决方案前端页面可以打开但接口全部报错后端服务没启动或端口不对检查 IDEA 或终端日志访问后端端口测试确认后端启动成功检查 Vite proxy 配置数据库连不上MySQL 没启动、用户名密码错误、数据库不存在检查后端日志用 Navicat 或命令行连接测试启动 MySQL核对 application.yml 配置Article 表字段变了启动报错JPA ddl-auto update 与实体映射不一致检查 JPA 日志输出的 SQL备份数据后重新建表或手动执行 ALTER TABLE登录成功后访问后台接口返回 401token 未携带或已过期打开浏览器开发者工具查看请求头确认前端请求拦截器从 localStorage 读取 token前端能访问但接口出现 CORS 错误跨域未配置看浏览器 Console 报错后端配置 CorsConfig或在 Vite 配置 proxy中文内容乱码表字符集不是 utf8mb4使用 SHOW TABLE STATUS 查看将库表和连接设置改为 utf8mb4发布文章后前台看不到status 不是 1查数据库状态字段通过后台修改状态确认 publish_time 非空文章新增成功但 content 丢失后端实体 longtext 映射问题检查实体字段类型在 JPA 实体字段上使用 Lob 声明内容字段9.1 端口冲突处理SpringBoot 默认占用 8080。如果你本机有别的服务占用了 8080启动时会报 Port already in use。最简单的处理是换端口修改 application.ymlserver: port: 8081前端 Vite 默认是 5173如果被占用Vite 会自动换到 5174所以大多数情况下不用手动处理。9.2 启动报出错误排查思路后端启动失败时优先关注异常堆栈最下面的 Caused by不要只看最上面一大段。比如出现Caused by: java.sql.SQLSyntaxErrorException: Unknown database tech_pop说明数据库没创建。这种情况直接进入 MySQL 执行建库语句即可。9.3 前端 npm install 失败如果 npm install 网络较慢或失败可以尝试切换 npm 镜像源npm config set registry https://registry.npmmirror.com npm install如果某个依赖包安装后启动报错优先删除 node_modules 和 package-lock.json 后重新安装rm -rf node_modules package-lock.json npm install这个方法解决大多数依赖版本漂移问题。当然实际生产部署时如果你使用的是企业内部私有 npm 仓库则必须按你的企业源来配置。9.4 数据库时间字段显示差 8 小时MySQL 连接串里带上 serverTimezone 是其中一种解决手段更稳妥的方法是 JVM 和 MySQL 都设置为 Asia/Shanghai 时区。另外CURRENT_TIMESTAMP默认值依赖数据库服务器时区如果服务器设置了 UTC写入的时间会差 8 小时。排查时先执行SELECT NOW()看数据库当前时间是否正常。10. 最佳实践与上线建议科普网站虽然功能不算复杂但要跑得稳并且被别人认可还是需要把工程细节做好。10.1 项目结构规范后端按 controller、service、repository、entity、dto 分层避免业务逻辑写在 controller 里。配置区分 dev 和 prod不要把生产数据库密码写在代码仓库里。前端 API 调用统一走封装后的 request不要每个页面直接 new axios。路由守卫统一校验登录状态不要在页面里散落一堆 token 判断。10.2 数据初始化正式上线前要先准备一套基础数据脚本管理员账号初始化首次启动时可以通过 CommandLineRunner 创建默认管理员。科普分类初始化能源动力、科技前沿等分类。package com.example.techpop.config; import com.example.techpop.entity.AdminUser; import com.example.techpop.repository.AdminUserRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; Component public class DataInitializer implements CommandLineRunner { private final AdminUserRepository adminUserRepository; public DataInitializer(AdminUserRepository adminUserRepository) { this.adminUserRepository adminUserRepository; } Override public void run(String... args) { if (adminUserRepository.count() 0) { AdminUser admin new AdminUser(); admin.setUsername(admin); admin.setPassword(new BCryptPasswordEncoder().encode(admin123456)); admin.setNickname(系统管理员); admin.setStatus(1); adminUserRepository.save(admin); } } }这段代码的意义在于你 clone 项目后第一次启动就能直接登录不需要手动往表里插密码。10.3 日志与监控后端打印日志要分级不要用 System.out 输出业务信息。Spring Boot 自带 Logback直接使用import org.slf4j.Logger; import org.slf4j.LoggerFactory;在业务方法里记录关键操作比如管理员登录成功、新增文章、批量下架等。日志文件保留周期建议 7 到 30 天避免磁盘被撑满。10.4 内容审核与发布规范科普类网站的公信力建立在内容准确性上。后台发布流程至少要加入“来源”字段文章详情页可以展示来源和作者信息。如果未来接入 AI 辅助生成内容必须在页面上做显著标识并设置人工审核环节。10.5 安全加固密码不能明文存储必须 BCrypt 加密。JWT 密钥长度不要少于 32 个字节建议通过环境变量注入。管理端接口除登录外都要校验 token。对外公开接口最好加 IP 频率限制避免爬虫刷接口。文件上传场景要限制文件类型和大小上传目录不能和前端静态目录混在一起。后端接口返回值不要直接打印所有异常堆栈给前端统一由全局异常处理器转换为友好提示。11. 后续扩展方向在现有三张表的基础上这个科普网站还可以继续扩展成更完整的科普内容平台增加文章标签表形成多对多关系文章详情页展示标签列表页支持按标签筛选。增加用户收藏和阅读历史功能提升访问者粘性。增加评论模块让读者可以围绕科普内容互动。此时需要重点考虑防灌水和敏感词过滤。引入搜索服务当文章量达到几千篇以上时可以用 Elasticsearch 替换数据库 like 查询。把资源上传、访问统计这些公共能力抽成独立服务方便后续多个站点复用。从开发角度讲用 JAVA SpringBoot3 Vue.js3 MySQL 做科普网站核心优势就是框架成熟、资料多、招聘需求大遇到问题能很快找到解决方案。把上面的工程结构和接口设计跑通后换一个内容主题比如“城市文化科普”“健康知识科普”只是多了一张分类表和不同页面文案整体骨架基本可以复用。如果你准备开始动手建议按这个顺序推进先按文中建表 SQL 把数据库建好把管理员表、分类表、文章表的数据初始化掉再启动后端用 curl 或者 Apifox 验证文章查询接口然后搭一个最简单的 Vue3 页面只展示文章标题列表等接口联调通了以后再慢慢补后台管理页面和发布功能。先跑通最小闭环后面扩展任何功能都会轻松得多。如果觉得这篇文章对排查思路和接口设计有帮助建议收藏备用。项目做完之后把数据库设计文档、接口文档和效果截图整理好这一套内容是完全可以写进项目经验里的。
返回列表