SpringBoot3+Vue3+MySQL:科普网站前后端分离实战解析
2026/9/5 17:39:01 网站建设 项目流程

这次我们来看一个非常典型的 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 字段。
  • 科普文章内容可能较长,正文建议使用TEXTLONGTEXT类型存储,富文本或 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 分类表 category
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='科普分类表';
3.2.2 文章表 article
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='科普文章表';
3.2.3 管理员表 admin_user
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_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.java

4.1 Maven 依赖示例

以下是核心的 pom.xml 文件内容节选,启动类按 SpringBoot3 常规写法即可。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.4</version> <relativePath/> </parent> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> </dependencies>

4.2 统一返回结果 Result 类

接口层不要直接返回 Map 或裸对象,最好定义统一的 Result 包装类,前端处理起来更省事。

package com.example.techpop.common; public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> 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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false 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: 24

4.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 JpaRepository<Article, Long> { Page<Article> findByStatusAndCategoryIdAndTitleContaining( Integer status, Long categoryId, String keyword, Pageable pageable); Page<Article> findByStatusAndCategoryId(Integer status, Long categoryId, Pageable pageable); Page<Article> 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 Result<Page<Article>> list(ArticleQuery query) { return Result.success(portalArticleService.listPublished(query)); } @GetMapping("/{id}") public Result<Article> 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 vite@latest techpop-web -- --template vue cd techpop-web npm install npm install vue-router@4 pinia axios element-plus

5.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-loading="loading"> <div v-for="article in articles" :key="article.id" class="article-card" @click="goDetail(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. 本地启动与前后端联调

先说结论:整个项目启动流程可以归纳为四步。

  1. 创建数据库并执行建表 SQL。
  2. 启动后端 SpringBoot 服务。
  3. 启动前端 Vite 开发服务器。
  4. 浏览器访问前端地址,验证接口连通性。

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 联调验证清单

联调阶段建议按下面顺序验证,不要跳步骤:

  1. 访客直接访问首页,确认能加载文章列表。
  2. 点击分类,确认列表按分类过滤。
  3. 点击文章,确认正文详情能正常展示。
  4. 未登录时访问后台地址,应被路由守卫重定向到登录页。
  5. 用初始化好的管理员账号登录,确认能获取 token。
  6. 登录后进入文章管理页,能查看所有状态的文章。
  7. 新增一篇草稿文章,在前台应该看不到,发布后前台才能看到。
  8. 批量勾选文章下架,确认前台列表同步变化。

其中第 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(List<Long> ids, Integer status) { List<Article> 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 里的科普文章批量导入到系统,应该单独设计一个导入任务,而不是通过后台表单逐条提交。比较稳妥的做法是:

  1. 前端上传 CSV 文件到后端。
  2. 后端解析文件,校验必填字段:标题、分类名称、摘要、正文。
  3. 按分类名称匹配已有分类,不存在则自动创建或忽略。
  4. 将合法数据批量保存,非法数据记录到失败列表。
  5. 返回成功导入数量和失败原因,前端下载失败记录。

这一套流程复杂度不高,但能极大提升后台内容维护效率。批量导入时的关键词提示:如果 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?page=0&size=10 \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx"

8. 批量任务与性能观察方法

科普网站的接口没有大模型推理那么重的资源消耗,但也不代表不用关注性能。前后端分离架构下的常见性能瓶颈主要集中在三个方面:慢查询、大字段传输、前端渲染数量过大。

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.ymlapplication-prod.yml,通过启动参数指定环境:

java -jar techpop-web.jar --spring.profiles.active=dev

这样的好处是数据库连接、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.yml:

server: 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 页面,只展示文章标题列表;等接口联调通了以后,再慢慢补后台管理页面和发布功能。先跑通最小闭环,后面扩展任何功能都会轻松得多。

如果觉得这篇文章对排查思路和接口设计有帮助,建议收藏备用。项目做完之后,把数据库设计文档、接口文档和效果截图整理好,这一套内容是完全可以写进项目经验里的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询