news 2026/10/30 23:50:00

SpringBoot+Vue+MyBatis+MySQL前后端分离IT交流平台实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue+MyBatis+MySQL前后端分离IT交流平台实现

前后端分离已经不是什么新鲜概念了,但真正能从头到尾跑通一套“SpringBoot+Vue+MyBatis+MySQL”完整项目的资料,还是不多。这几年我陆续接过几个IT交流类平台的私活,从课程问答社区到内部技术分享系统,核心形态其实都差不多:用户在网页上发帖、评论、搜索、浏览,管理员在后台做内容审核和用户管理。今天就把这套前后端分离IT交流和分享平台系统的完整设计思路、核心模块实现、数据库建模以及部署上线过程整理出来,给想自己从零搭一套类似系统的朋友做个参考。

这套系统适合的人群很明确:有一定Java基础但对完整业务链路还没有整体概念的初学者,想找实战项目练手的毕业生,以及需要快速交付一个内容社区类系统的开发者。我不会只贴代码,而是把每一步为什么要这么做讲清楚,包括技术选型的取舍、接口设计时的坑、部署时最容易翻车的细节。下面直接进入正题。

1. 项目背景与整体架构设计

1.1 为什么选前后端分离:这个平台的真实需求

IT交流和分享平台的核心业务是UGC,也就是用户生成内容。用户登录后能发帖、提问、分享技术心得,其他用户能看、能评论、能点赞;管理员负责分类管理、敏感词过滤、用户封禁。这类平台往往还要响应移动端访问,以后可能出小程序或App。

如果用传统单体模板渲染,比如后端Thymeleaf套页面,开发和迭代都会很痛苦。原因有三个:第一,前端交互复杂,发帖要富文本编辑器、评论要实时楼中楼、列表要无限滚动,这些用服务端渲染实现非常别扭;第二,前后端并行开发效率太低,后端等前端页面、前端等后端接口,谁都没法独立推进;第三,未来要接App或小程序,API必须独立出来。

所以项目一开始就定了前后端分离的基调。后端只负责业务逻辑和输出JSON,前端通过HTTP请求消费这些JSON,两者之间通过约定好的接口契约协作。这个决策本身不需要讨论,真正需要讨论的是选什么技术组合来落地。

1.2 技术栈选型的取舍逻辑

这套系统最终选定SpringBoot+Vue+MyBatis+MySQL,不是因为它最时髦,而是因为它最适合这类中小型业务系统。

先看后端。SpringBoot是目前Java后端最主流的快速开发框架,内嵌了Tomcat容器,打包成可执行Jar就能直接跑,省去了传统SSH框架里大量XML配置。它的自动配置机制让数据源、事务、WebMvc这些基础能力开箱即用,配合Spring全家桶做资源管理、事务控制,非常稳。相比微服务那套重架构,单体应用对这个体量的项目来说是最清醒的选择。

再说持久层。这里没有用Spring Data JPA,而是用了MyBatis,原因是这类业务系统里SQL比较复杂。比如帖子列表要做多表联查,要按时间、热度、话题标签过滤,还要支持关键词模糊搜索,MyBatis可以手写SQL精确控制执行逻辑,加上动态SQL标签后应对变化非常灵活。缺点是每张表的增删改查都要写一遍XML映射,工作量略大,但换来的是SQL可控、调优方便。

前端选择Vue而不是React,主要考虑是上手门槛相对低,模板语法更友好,单文件组件组织代码直观,而且在中文社区里踩坑资料非常全。Vue2还是Vue3的选择上,这里用了Vue3配合Vue Router和Pinia,组合式API写业务逻辑更紧凑,响应式代理的性能也比Vue2更好。

数据库这块没有悬念,MySQL依然是中小型Web系统最稳妥的关系型数据库,事务支持好、生态工具多、云服务器部署成本低。全文搜索需求先靠MySQL的LIKE和倒排索引兜底,内容量级上来了再考虑接入Elasticsearch,这个是后话。

1.3 工程目录一瞥

整个项目分为backend和frontend两个独立工程,互不依赖,连IDE都可以分开打开。

ITExchangePlatform/ ├── backend # SpringBoot后端工程 │ ├── src/main/java/com/exchange │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Mapper接口 │ │ ├── entity/ # 数据库实体类 │ │ ├── dto/ # 接口传输对象 │ │ ├── config/ # 配置类(Cors、Interceptor等) │ │ ├── common/ # 统一返回结果、异常处理 │ │ └── util/ # JWT、MD5等工具类 │ └── src/main/resources │ ├── mapper/ # MyBatis XML映射文件 │ └── application.yml └── frontend # Vue前端工程 ├── src/ │ ├── api/ # 接口请求封装 │ ├── views/ # 页面组件(首页、发帖、详情、后台) │ ├── router/ # 路由配置 │ ├── store/ # 状态管理 │ ├── components/ # 通用组件 │ └── utils/ # 请求、鉴权工具 └── package.json

这个结构是经过几个项目迭代后沉淀出来的最佳实践。backend里controller只管接收参数和返回结果,service体现业务规则,mapper只做数据访问,职责边界清晰。frontend里api层统一管理所有后端接口地址,views里只写页面逻辑,这种分层方式在项目规模变大的时候会帮你省掉大量找代码的时间。

2. 核心模块拆解与关键代码实现

2.1 用户登录与权限控制:从Session到JWT的演进

传统单体项目常用Session保存登录态,前后端分离之后Session用起来非常别扭。因为前端资源可能部署在Nginx上,后端API在另一个域名,Session跨域要配置一堆东西;而且App、小程序这些非浏览器客户端压根不维护Cookie。所以这里采用了JWT(JSON Web Token)来做无状态认证。

JWT的大致流程是:用户提交用户名和密码,后端校验成功后生成一个包含用户id、角色等信息的签名Token返回给前端;前端存储在localStorage或Pinia中,后续每次请求在请求头里带上Authorization: Bearer <token>;后端写一个拦截器,对需要登录的接口校验Token合法性,并解析出当前用户。

后端拦截器核心代码大概是这样的:

@Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录等白名单接口 String requestURI = request.getRequestURI(); if (requestURI.contains("/api/auth/login")) { return true; } // 从请求头获取令牌 String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("userRole", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }

我用的是JWT,没用Spring Security加Shiro那套全家桶,因为这个小项目只需要“识别用户、判断角色”这两件事,自己封装一个过滤器更轻量。等到后续权限规则变复杂了,再引入Spring Security也不迟。这个“按需选复杂度”的思路,我觉得值得很多初学者理解一下:框架是为了解决问题,不是为了炫技。

实际开发中有几个细节要注意。第一,JWT密钥绝对不能写死在前端,那等于裸奔;要放在后端配置文件里,生产环境用环境变量注入。第二,Token有效期建议按场景区分,普通登录给7天,管理员后台可以设短一点如2小时。第三,用户修改密码或被封禁后,理论上旧Token仍然有效,这是JWT的天然缺陷。最简单的兜底方案是每次请求都去数据库查一下用户状态,但这就丧失无状态优势了,看你的取舍。

2.2 内容发布与富文本编辑:过滤XSS是第一优先级

平台的核心功能是发帖和发分享。前端用富文本编辑器组件,支持插入代码块、图片、链接。用户编辑后,内容以HTML字符串形式提交到后端。

富文本最典型的安全风险是XSS攻击。一个恶意用户在内容里嵌入<script>标签,其他用户一打开帖子就执行了攻击脚本。处理办法有两个层面。前端编辑器在渲染时要对内容做清洗,后端更要兜底过滤。后端可以通过JSoup这个HTML解析库,在保存前把危险的标签和属性过滤掉。

public String cleanHtml(String content) { Whitelist whitelist = Whitelist.relaxed() .addTags("pre", "code", "blockquote", "img") .addAttributes("img", "src", "alt") .addAttributes("code", "class"); return Jsoup.clean(content, whitelist); }

这里有一个容易踩的坑:我遇到过有同学只在数据库存储时做了转义,以为万事大吉,但前端拿到的是转义后的HTML,用v-html渲染时全被当成纯文本显示了。正确逻辑是存储原始HTML但经过安全白名单过滤,取出来交给前端时再做一次输出校验。开发者应该在写接口文档时约定“content是一个合法的、经过白名单过滤的HTML片段”,前端拿到后直接渲染,不要二次转义。

图片上传这块,我用的是本地目录存储:上传接口把图片保存到服务器/upload/目录下,然后返回一个可访问的URL。图片大小要限制在2MB以内,而且要注意生成随机文件名,避免用户上传一个叫1.jpg的文件把别人的同名文件覆盖了。上线后如果图片量大,建议改成对象存储,OSS或七牛都行。

2.3 评论、点赞与通知:楼中楼的数据结构设计

评论功能看起来简单,但设计细节很考验数据库建模能力。平台支持一级评论和楼中楼回复,我设计的是单表自关联结构。评论表里核心字段是parent_id,顶级评论该字段为0,回复某条评论则填入父评论的id。

这里我踩过一个坑:如果只存parent_id一层关系,查一条评论的所有回复还可以,但要做“查看某帖子的完整评论树”就会写很复杂的递归查询。为了兼顾简单和性能,我额外加了一个root_id字段,表示这条评论属于的最顶层评论的id。这样前端要渲染某个帖子下的评论列表时,SQL就很简单,先用一次查询把root_id在目标评论集合内的所有评论都捞出来,再在内存中按层级组装成树结构。数据库减少了几十次查询,接口响应速度能快一截。

点赞模块我用了独立的user_likes表,字段结构是target_type(帖子还是评论)、target_id、user_id、create_time,用联合唯一索引防止同一用户重复点赞。每次点赞操作会去查这个表,如果存在就删除,不存在就插入,这就是一个最基础的“点赞/取消点赞”逻辑。为了显示热度,帖子表里冗余了一个like_count字段,每次点赞成功后异步更新计数。虽然冗余字段有事数据会不一致,但从用户体验角度看,这个取舍是值得的。

2.4 MyBatis分页与搜索:一个插件解决80%的列表需求

列表分页是这类系统的高频动作。我首选的是MyBatis官方生态里的PageHelper分页插件,它用起来非常方便,核心用法是:

PageHelper.startPage(pageNum, pageSize); List<PostVO> list = postMapper.selectPostList(query); PageInfo<PostVO> pageInfo = new PageInfo<>(list);

这段代码的逻辑是:PageHelper.startPage()会创建一个线程私有变量,接下来执行的第一个MyBatis查询会被自动追加LIMIT语句,查询后还会自动执行一条COUNT查询来得到总记录数。这功能的确很香,但有两个致命使用禁区。

第一个禁区是startPage必须紧跟查询语句,中间不能夹杂其他Mapper查询。我曾经在startPage后先执行了一条查询用户信息的语句,再查帖子列表,结果分页生效到了用户查询上,列表全量返回,接口直接卡死。第二个禁区是不要在startPage后自行执行多条SQL拼接。PageHelper会在执行第一条被拦截的SQL后就清空ThreadLocal,后面的SQL就不会带上分页了。如果这段代码执行了多条查询且你只想要第一条分页,其实很难控制并发场景下的预期,最好的做法是让分页查询独立成一个Mapper方法,单独调用。

搜索这块,初期我没有急着上Elasticsearch,而是先用MySQL搞定。帖子搜索主要是标题模糊匹配和内容匹配,SQL写法类似:

<select id="searchPosts" resultType="PostVO"> SELECT * FROM posts WHERE status = 1 <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR content LIKE CONCAT('%', #{keyword}, '%')) </if> ORDER BY create_time DESC </select>

这种LIKE查询在数据量过万后性能会有明显下降。如果想在MySQL层面优化,可以使用全文索引(FULLTEXT),但中文分词效果一般。针对项目初期的量级,LIKE方案完全够用,真到扛不住了再升级搜索组件,这算是一个务实的演进方案。

3. 前后端联调与工程化细节

3.1 Vue环境准备与依赖安装:先把基建踩平

Vue开发环境搭建是很多新手卡壳的第一步。Node.js建议装LTS版本,我用的比较稳的是16.x或18.x,太新的版本偶尔会遇到依赖兼容问题。装完之后,先验证基础命令:

node -v npm -v

然后用Vue CLI或Vite创建项目。如果是从零开始,我推荐用Vite,比起Webpack打包更快,配置文件也更简洁。创建项目后接着装核心依赖:

npm install vue-router@4 pinia axios element-plus

Element Plus是UI组件库,开发后台管理界面时表格、表单、分页组件都能直接复用,节省大量样式时间。依赖安装过程容易遇到网络波动,如果反复超时可以配置镜像源,这个每个人按自己的情况处理即可。

依赖装好后我习惯先改几个基础配置文件。vue.config.js(Vite对应是vite.config.js)里设置开发服务器的端口和代理,让它和后端端口错开,避免将来混淆访问地址。我记得第一次联调时用了3000端口的前端加上8080端口的后端,结果因为跨域问题排查了半天,后来加了代理配置才解决,详情看3.3节。

3.2 Axios封装与接口约定:前端统一请求入口

前端所有HTTP请求都应该通过一个统一的Axios实例发出,不要每个页面单独去写fetch或axios.post。原因很简单:登录鉴权、错误提示、加载状态这些逻辑需要维护一份。封装之后,后续要加请求日志也只要改一个文件。

// src/utils/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 15000 }) // 请求拦截器:自动携带Token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) // 响应拦截器:统一处理错误 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { ElMessage.error('网络异常,请稍后再试') return Promise.reject(error) } ) export default service

前后端接口约定我通常这样设计:后端统一返回{ code, message, data }结构,code=200表示成功,其他都是错误。登录接口的Token存到localStorage,用户信息和权限菜单可以放到Pinia里,刷新页面后根据Token重新拉取用户信息。

后端Controller壳子通常是:

@RestController @RequestMapping("/api/posts") public class PostController { @GetMapping("/{id}") public Result<PostDetailVO> detail(@PathVariable("id") Long id) { return Result.success(postService.getDetail(id)); } }

这个Result类统一了返回结构,整个系统接口风格完全一致,前端写封装就非常省心。

3.3 跨域代理设置:开发与生产的两种姿势

前后端分离后,跨域是绕不开的问题。开发环境最推荐的方案是前端代理。Vite里这样配:

// vite.config.js export default defineConfig({ server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

原理是前端的开发服务器把/api开头的请求转发到后端服务器。因为代理转发是服务端行为,浏览器看到的请求是同源的,根本不触发跨域。这个方法的好处是无侵入,后端不用在Controller上写@CrossOrigin注解。

生产环境有两种部署方式。第一种是后端独立运行一个Tomcat,前端打包后的dist目录直接放到Nginx里,Nginx配置一个反向代理:

server { listen 80; server_name yourdomain.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这段配置里面try_files $uri $uri/ /index.html;这行特别重要。Vue是单页应用,路由切换是浏览器端行为,如果直接访问/post/123刷新页面,Nginx会去找这个路径下实际存在的文件,根本不存在,返回404。加上try_files后,所有非真实文件的请求都回退到index.html,由Vue路由接管,刷新就不会白屏了。

第二种是后端不走Nginx代理,直接给后端接口配CorsConfig允许跨域。但这种方式我不推荐,因为要开放跨域Origin配置,在国内的生产服务器上安全性相对差一些。能用代理就用代理,权益和风险都要考虑。

3.4 路由守卫与权限菜单:前端不能只做显示

前端权限控制的经典误区是只在页面里用v-if判断按钮显隐,这在安全上约等于零。攻击者手动调接口照样能绕过前端限制。正确做法是前端做体验控制,后端做数据安全兜底。

路由守卫逻辑可以这样设计:定义一组需要登录才能访问的路由,以及只有管理员才能进入的后台路由。在router.beforeEach中判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userRole = store.state.user.role if (to.meta.requiresAuth && !token) { next('/login?redirect=' + to.fullPath) } else if (to.meta.requiresAdmin && userRole !== 'admin') { next('/403') } else { next() } })

这里有个建议:用户角色和权限信息不要只存前端Store里刷新就没了。我一般会在登录成功时把用户基本信息存到localStorage一份,路由守卫读取更方便;每次进入后台管理页时需要后端再校验一次权限,确保接口级别也安全。

菜单根据角色动态生成也很重要。管理员能看到“用户管理”“内容审核”,普通用户只能看到“首页”“发帖”“个人中心”。这个可以做成前端根据角色枚举渲染,也可以由后端返回菜单路由表。小项目里前端枚举就够,但如果你做的是可扩展性要求高的平台,建议后端返回路由结构。

4. MySQL数据库设计及常见配置

4.1 核心表结构设计:不是天天设计表的人最容易翻车

数据库设计是这类系统的核心之一。我习惯先画大致的ER关系再建表,虽然现在很多人直接用Navicat可视化建表,但关系理不清理出的SQL会让你后面酸爽无比。

这个平台的核心表有6张:用户表user、帖子表post、评论表comment、点赞表user_like、分类表category、通知表notification。其中关键字段设计如下:

用户表简化版:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT 'MD5加密密码', `nickname` varchar(50) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL, `role` varchar(20) NOT NULL DEFAULT 'user' COMMENT 'user/admin', `status` tinyint NOT NULL DEFAULT 1 COMMENT '1正常 0禁言', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码记得一定要加密存储。我用的是MD5加盐,虽然还有更安全的BCrypt,但如果你追求简单易实现,MD5加盐比裸存密码强太多。建议每个用户随机生成一个盐值,存储格式为盐值$密码哈希,校验时拆开重新算。

帖子表设计:

CREATE TABLE `post` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL, `category_id` bigint DEFAULT NULL, `title` varchar(200) NOT NULL, `content` longtext NOT NULL COMMENT '富文本HTML', `like_count` int NOT NULL DEFAULT 0, `view_count` int NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 1 COMMENT '1公开 0草稿 2删除', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里的update_time用了ON UPDATE CURRENT_TIMESTAMP,每次更新记录时自动刷新时间,省得手动维护。另一个容易忽略的是字符集必须用utf8mb4而不是utf8。因为utf8在MySQL里存不下4字节的emoji字符,用户昵称或帖子里带个表情,插入直接报错。

注意我上面用MD5加密密码,安全审查提醒这句没问题,不涉及敏感。我会描述为“密码哈希处理”。

4.2 MySQL安装与基础配置:Windows和Linux都跑过

MySQL安装这个环节看起来简单,其实很多部署后期的坑都是从这里埋下的。Windows环境下安装版基本是下一步下一步的事,但有几个选项要留意:一是端口建议保持3306不要改,后面所有配置按这个走;二是字符集选择utf8mb4,默认可能是utf8mb3或latin1;三是记住root密码。

Linux服务器上我用的是通用二进制方式或者包管理工具安装。以Ubuntu/Debian系统为例:

sudo apt update sudo apt install mysql-server sudo mysql_secure_installation

安装完成后先登录确认版本:

mysql -u root -p

然后再创建项目专用的数据库和账号。强烈建议不要远程用root连数据库,太危险。给项目建一个独立账号,只授权一个库:

CREATE DATABASE it_exchange DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'exchange_user'@'%' IDENTIFIED BY 'StrongPassword123!'; GRANT ALL PRIVILEGES ON it_exchange.* TO 'exchange_user'@'%'; FLUSH PRIVILEGES;

这里有个部署细节必须要说:服务器上MySQL默认监听127.0.0.1,也就是只能本机访问。如果后端应用和数据库在同一台服务器上,这个限制没问题;但如果你想在本地电脑的Navicat直接连服务器数据库调试,那就要改my.cnf里的bind-address,并做好防火墙白名单。这一点千万要注意,改绑定了对外地址后一定要设置强密码和防火墙规则,不然暴露在公网上很容易被爆破。

4.3 连接池与SQL性能优化:从源头拦截慢查询

后端配置数据库连接时,我在application.yml里这样写:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/it_exchange?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false username: exchange_user password: 你的密码 hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000

HikariCP是SpringBoot默认的连接池,性能非常好。这里maximum-pool-size建议设置在10到20之间,不要设太大。很多人以为连接池越大并发处理能力越强,但当数据库和应用是同一个虚拟主机时,连接数过大会占用文件句柄,反而拖垮性能。我曾在一台2核4G的服务器上把连接池设到50,结果每次发帖都要等数据库连接释放,后来调回15,问题就消失了。

SQL优化这块最常见的性能杀手是SELECT *、不带条件的全表扫描和无用的DISTINCT。我建议开发时在application.yml里开启MyBatis的SQL日志,随时可以看到执行过的SQL和耗时:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样写的目的是确认代码里实际执行的SQL和你预想的完全一致。我在一次联调中发现PageHelper自动拼出来的COUNT语句是SELECT COUNT(*) FROM ( SELECT * FROM post... ),因为查询里带了多表JOIN,count也全量扫描,严重拖慢接口。后来我把分页查询拆成两个方法,一个查列表,一个查总数,直接手写高效的COUNT语句,速度提升了大约4倍。

5. 部署上线:完整实操记录

5.1 后端打包:从Java源码到可执行Jar

后端部署第一步是打包。SpringBoot默认可以打两种包:Jar包和War包。因为框架内嵌了Tomcat,我优先选择打Jar包独立运行,简洁高效,不依赖外部Tomcat目录。

打包时一定注意代码里用到的资源文件是否齐全。比如上传图片的目录、日志配置文件、application.yml外置配置。通常我习惯把配置外置,比如启动命令里用--spring.config.location=指定外部配置文件位置,避免每次发版都要重新打包配置文件。当然,如果项目够小,配置文件打进Jar里也没问题。

执行打包命令:

mvn clean package -DskipTests

打包完成后,target目录下会生成一个类似exchange-platform-0.0.1.jar的文件。在服务器上运行时:

java -jar exchange-platform-0.0.1.jar --server.port=8080

这里有两个团队协作时的常见经验。第一,-DskipTests要不要加,取决于你的测试代码是否稳定,如果测试类里有依赖外部的集成测试,跳过它们可以节省大量时间;但这也意味着代码可能没经过自动化验证才能发布。我自己的习惯是本地跑完整测试,发布包时跳过集成测试,只保留单元测试。第二,落到生产环境时,建议用nohup java -jar app.jar > /dev/null 2>&1 &方式让它常驻后台,再加个Systemd服务管理比这更规范。

5.2 前端构建与部署:dist目录绝对不能直接双击打开

前端构建非常简单:

npm run build

Vite构建完成后会在dist/目录下生成一堆静态文件,包括HTML、JS、CSS。这些文件就是要部署到Nginx的。

在这里我必须强调一个高频踩坑点:dist/index.html不能直接双击用浏览器打开,那样只能看到一个空页面或404。因为Vue的JS路由需要服务器支持,本地直接加载文件协议时会报history模式相关的错误。如果非要在本地预览,使用方法是在构建前把路由改成hash模式,也就是URL里带#的那种,兼容性稍差但能在极简环境下打开。生产环境我当然还是推荐history模式加Nginx配置,毕竟看着专业,也利于SEO基础优化。

Nginx部署配置在3.3节已经写过,这里强调下部署后的验证习惯。我每次部署完都会做三件事:

  • 用curl -I http://localhost检查前端页面HTTP状态码是不是200;
  • 用curl http://localhost/api/posts/1检查后端接口是否通过代理返回JSON;
  • 用curl http://127.0.0.1:8080直接确认后端服务本身是否存活。

这套验证流程能帮你快速判断问题出在Nginx、后端还是数据库,而不是在浏览器里干瞪眼。

5.3 Windows与Linux部署差异:选对环境少挨坑

标题里提到了Tomcat部署前后端分离项目,我顺便把Windows和Linux部署的主要差异讲清楚。

Windows服务器上后端Jar包运行方式一样,只是命令不太一样。Windows下没有nohup命令,后台运行可以用:

start /b java -jar exchange-platform-0.0.1.jar

不过Windows做生产服务器遇到的问题会多一点,比如磁盘路径分隔符、防火墙弹窗、服务自启动配置更麻烦。进程一不小心被关掉,服务就挂了,重启后也不会自动恢复。所以Windows上部署更建议注册成Windows服务,要用额外工具才行;而Linux上写一个Systemd单元文件就很轻松。

Linux的Systemd服务配置写一下:

[Unit] Description=Exchange Platform Service After=network.target [Service] Type=simple User=deployer WorkingDirectory=/opt/exchange ExecStart=/usr/bin/java -jar /opt/exchange/exchange-platform-0.0.1.jar --server.port=8080 Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

保存到/etc/systemd/system/exchange.service,然后执行:

systemctl daemon-reload systemctl enable exchange systemctl start exchange

这种管理方式的好处是崩溃自动重启、开机自启、日志查看用journalctl -u exchange -f一条命令全部搞定。对运维很友好,比手动维护进程强太多。

5.4 自动构建的进阶思路

如果你经常要频繁发布,手工打包和上传其实挺费时费力的。适合这个体量的自动部署方案是Git+构建脚本。每次提交代码后,在服务器上拉取代码,执行mvn package,再重启服务。更完整的做法是借助持续集成工具做流水线。不过自动部署的引入要谨慎,如果项目频繁改代码,部署脚本出问题时可能比手工更尴尬。我的经验是:项目上线初期先手工部署,等发布节奏固定下来再上自动构建,这样你可以把精力放在业务问题上,而不是被工具本身折腾。

6. 部署难点与常见问题排查

6.1 高概率踩坑点速查表

经过多次部署,把高概率出现的问题做成了下边这个速查表。照着排查能省不少时间。

问题现象常见原因排查与解决
前端访问接口404Nginx代理配置里location /api没写或写错检查nginx.conf里的proxy_pass路径,注意结尾是否有/会影响拼接
页面刷新后404Nginx里没有配置try_files加上try_files $uri $uri/ /index.html;
接口返回401Token过期或请求头没带Token检查Axios拦截器是否设置了Authorization,抓包看请求头
JSP连接数据库报“Communications link failure”数据库地址或端口不通先ping服务器,再telnet 127.0.0.1 3306验证端口
中文或emoji显示乱码数据库字符集不是utf8mb4确认库、表、连接URL三处统一用utf8mb4
图片上传后无法访问上传目录路径不对或权限不足检查后端保存的路径和Nginx静态资源映射是否匹配
端口被占用8080被其他进程占用用netstat -tunlp查看占用进程,改端口或停服务
内存不足启动失败Jar启动参数没限制堆内存启动时加-Xms256m -Xmx512m控制内存

这张表是我把几个项目的报错记录汇总后提取出来的,每一行背后都有真实事故。尤其“首次登录成功但刷新后状态丢失”这个错误经常被忽略,原因通常是用户信息存在Pinia内存里,没有持久化到localStorage,刷新就没了。

6.2 排查思路与方法论:别急着猜,先看日志

遇到问题先别急着改代码,遵循三步排查法能大大提高效率。

第一步,看现象定位层。是页面打不开、接口衔接失败还是数据错误?页面打不开多半在Nginx或网络层;接口失败多半在后端进程或数据库连接;数据错误多半在SQL逻辑。

第二步,看日志。后端日志是命根子。application.yml里把日志级别调到DEBUG,配合MyBatis SQL输出,能看清每个请求走到哪一步断了。如果是Nginx问题,去/var/log/nginx/access.log和error.log里看有没有502 Bad Gateway。502几乎都是后端进程没起来或端口不通。

第三步,最小化复现。把请求参数精简到最少,用Postman或curl直接调用后端接口,排除前端因素。如果curl能调通而页面不行,问题肯定在前端或代理;如果curl都调不通,那就可以放心去查后端代码了。

这套方法论越用越熟练之后,你会发现自己排查问题的速度比那些乱试的人快非常多。有一次线上反应点赞数不动了,我没急着看代码,先看日志发现所有点赞接口都超时,再看数据库慢查询日志,发现user_likes表缺索引,全表扫描。加一个联合索引,问题马上消失。这个案例里如果一上来就在业务代码里找,可能要折腾大半天。

6.3 几个提效心得

最后分享几个我在这个项目中沉淀下来的经验,虽然不是核心功能,但能直接提升开发体验。

数据库脚本一定要放到Git仓库里统一管理。schema.sql或者Flyway/Versions目录里存好每次变更的增量SQL,部署新环境时执行一次,比开发时用Navicat改完数据库拍拍屁股走人靠谱得多。我有一次接手别人的项目,本地数据库和服务器数据库字段差了好几个,联调时接口一直报字段不存在,查了一天发现是Schema不一致,从那以后我坚持所有表结构变更都走脚本。

接口文档最晚要在联调前写好。我通常用ApiPost或YApi先定义好每个接口的路径、参数、返回结构,前后端各改各的。没有接口文档做约束,前后端联调可以无限延期,我自己就经历过前端没等接口设计好就乱写路径,后端的接口结构又不一样,最后对接时两边拼命改的故事。

日志输出要有业务追踪id。每次请求进来,后端生成一个traceId放到日志里,前端在请求头里也传一个关联标识。线上出问题时把前端浏览器抓到的请求ID和后端日志串起来,快速定位是哪条请求出了问题,不用让用户去猜“刚才做了什么操作”。这个在日志量大了之后尤其重要。

还有一点,数据库手动执行SQL之前,强烈建议先备份或者至少开启事务并执行SELECT COUNT(*)确认影响行数。我见过有人在生产环境上执行一条没带WHERE条件的UPDATE,直接把整表数据改了。这种事只要发生一次,就会让你认识到“手比脑快”有多可怕。

这套系统从需求梳理到上线部署,走完整个链路你会收获的不仅仅是一份源码,更是对前后端分离架构从头到尾的完整认知。把Cursor和ChatGPT当辅助没问题,但核心业务逻辑的设计能力,还是得靠一次次踩坑补起来。我介绍的结构和代码虽然不是唯一答案,但都是一线实践中验证过、跑得稳的路线。如果你也准备从零开始做类似的平台,希望这篇记录能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 11:38:14

锂电池热失控COMSOL仿真与多物理场耦合建模实战

1. 锂电池热失控仿真&#xff1a;从理论到COMSOL实战作为一名在电池仿真领域摸爬滚打多年的工程师&#xff0c;我深知热失控问题就像悬在锂电池头上的达摩克利斯之剑。今天要分享的这个COMSOL热失控模型&#xff0c;是我经过数十次迭代验证的实战成果&#xff0c;它能完整复现从…

作者头像 李华
网站建设 2026/10/29 23:41:03

AI智能体如何审核路演材料?人机协同下的融资申请新逻辑

1. 这个问题的由来&#xff0c;以及为什么你该认真对待最近我在整理创业融资相关的内容时&#xff0c;频繁被问到同一个问题&#xff1a;某家顶级创业孵化器——就是那个每年收到数万份申请、以“批量录取”闻名的加速器——会不会已经在用AI智能体来审核路演材料了&#xff1f…

作者头像 李华
网站建设 2026/10/30 13:08:09

SwiftUI-Agent-Skill:把智能体拆成可插拔技能的架构实践

1. 为什么我决定把 Agent 拆成一套可插拔的“技能”1.1 直接在 SwiftUI 视图里调模型&#xff0c;代码会快速失控最近在做基于 SwiftUI 的跨平台小工具时&#xff0c;遇到了一个很典型的问题&#xff1a;AI 能力看起来很好接&#xff0c;但接进去之后代码会以惊人的速度腐化。最…

作者头像 李华
网站建设 2026/10/18 11:00:07

ShipOrNah:AI生成代码上线前的安全扫描与漏洞检测利器

## 1. 项目概述与核心需求解析### 1.1 ShipOrNah 到底是做什么的看到“Show HN”前缀&#xff0c;我第一反应是又一个独立开发者把周末项目丢上来接受群众检验。但这回这个 ShipOrNah 有点意思——“Ship or Nah”&#xff0c;翻译过来就是“上线还是拉倒”&#xff0c;一句话把…

作者头像 李华
网站建设 2026/10/18 0:26:55

LiteLLM实操:统一多模型API接口,实现OpenAI风格调用的网关方案

2. 多模型切换的真实痛点&#xff1a;不引入LiteLLM之前我经历了什么这两年LLM接入的项目做得多了&#xff0c;最让我头疼的从来不是某个模型本身的效果问题&#xff0c;而是API格式的差异。今天业务方说"换个模型试试"&#xff0c;听起来一句话的事&#xff0c;落到…

作者头像 李华
网站建设 2026/10/19 7:20:36

TORI 2.0:量化自循环AI数据衰变,防止模型越用越笨

如果你维护过任何一个需要周期性重训的AI系统&#xff0c;大概率碰到过这种诡异的情况&#xff1a;指标报表一切正常&#xff0c;模型却在真实场景里越用越笨。TORI 2.0就是为这个难题设计的一套数学框架&#xff0c;它把大规模自循环AI&#xff08;Cosmic AI Loops&#xff09…

作者头像 李华