简介:一份基于SpringBoot+Vue的助农电商平台完整项目源码,主要面向高校毕业设计、课程作业以及Java全栈学习者,同时适用于希望了解电商系统前后端分离实现路径的开发者。平台以农产品销售场景为核心,涵盖商品展示、用户管理、订单处理等业务模块,后端基于SpringBoot的“约定优于配置”特性降低搭建门槛,并通过数据库表结构设计保障产品分类、用户信息、订单数据等的一致性;前端使用Vue构建动态交互界面,代码划分为client_code、manage_code、server_code等目录,清晰体现客户端、管理端与服务端逻辑分离,便于独立部署和二次开发。压缩包共375个文件,约10.89MB,以java、vue、js、css等源码为主,并包含png/jpg界面截图、xml配置文件、sql数据库脚本以及“有问题请先读我.txt”常见问题文档,可帮助读者快速完成从环境搭建到功能调试的全流程学习。已有162人学习下载,适合作为毕业设计参考、课程项目模板或农贸电商入门实战案例,在现有基础上扩展营销、支付等模块也较为方便。
1. 助农电商平台:先想清楚它在毕设里到底考什么
“助农电商平台”这类题目,在 Java 课程设计和毕业设计里出现频率很稳定。表面看它是从 0 到 1 写一套电商系统,实际上核心工作量集中在三块:角色权限拆分、商品-订单数据模型、前后端联调。如果你现在打开源码包一头扎进代码,大概率三天后还在改接口路径。建议先看数据表,再看后端接口,最后打开前端,这套顺序能帮你把“助农”这个大命题收敛成七个表加二十个接口的工程题。适合正在做毕设的 Java 学生,也适合想用 SpringBoot + Vue 刷一遍电商流程的开发者。
2. 需求拆解与数据库设计:把“助农”收敛成七个核心表
拿到基于 SpringBoot + Vue 的助农电商项目,我最建议先看数据库脚本。因为电商项目里,接口可以临时补,字段关系错了,回改成本极高。如果资源包里没带sql文件,按下面这套结构建出来,后面写接口会非常顺。
2.1 角色划分与权限边界
助农平台天然有三类人:管理员、农户、普通用户。角色设计直接决定后端接口要不要做权限校验,也决定前端路由怎么拦截。下表是我在毕设里常用的权限边界:
| 角色 | 主要操作 | 接口前缀 |
|---|---|---|
| 管理员 | 用户管理、商品审核、订单查看、数据统计 | /api/admin/** |
| 农户 | 商品发布、商品编辑、订单发货 | /api/farmer/** |
| 普通用户 | 浏览商品、加购、下单、查看订单 | /api/user/** |
用户表里的角色字段,建议直接存整型而不是字符串:
`role` tinyint(1) NOT NULL DEFAULT 2 COMMENT '0-管理员 1-农户 2-普通用户'为什么不用字符串:前端if (user.role === 1)判断最直接,数据库体积也小。如果答辩时导师问角色扩展,注释里已经写了“预留扩展位”,一句就能解释清楚。这体量的项目没必要建角色表和用户-角色关联表,那套东西留到 SSM 老项目里反而显得臃肿。
2.2 核心表结构与建表脚本
我梳理出的七个核心表是:用户表sys_user、分类表category、商品表product、购物车表cart、订单表order_info、订单明细表order_item、收货地址表address。下面给出最关键的四个表建表脚本,分类和地址表结构简单,按外键思路补就行:
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL COMMENT '登录名', `password` varchar(128) NOT NULL COMMENT '加密后的密码', `nickname` varchar(32) DEFAULT NULL COMMENT '昵称/农户名称', `role` tinyint(1) NOT NULL DEFAULT 2 COMMENT '0-管理员 1-农户 2-普通用户', `phone` varchar(11) DEFAULT NULL, `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '0-禁用 1-正常', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(64) NOT NULL COMMENT '农产品名称', `category_id` bigint(20) NOT NULL COMMENT '分类ID', `price` decimal(10,2) NOT NULL COMMENT '单价(元)', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存', `unit` varchar(8) DEFAULT '斤' COMMENT '售卖单位', `cover` varchar(255) NOT NULL COMMENT '主图地址', `images` text COMMENT '轮播图,JSON数组', `detail` text COMMENT '富文本详情', `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0-下架 1-上架 2-待审核', `farmer_id` bigint(20) NOT NULL COMMENT '所属农户ID', `deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` bigint(20) NOT NULL, `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '0待付款 1待发货 2待收货 3已完成 4已取消', `receiver_name` varchar(32) NOT NULL, `receiver_phone` varchar(11) NOT NULL, `receiver_address` varchar(255) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `product_id` bigint(20) NOT NULL, `product_name` varchar(64) NOT NULL COMMENT '下单快照', `product_cover` varchar(255) DEFAULT NULL, `price` decimal(10,2) NOT NULL COMMENT '下单单价', `count` int(11) NOT NULL DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个设计容易忽略:order_info里冗余了收件人姓名、电话、地址,而收货地址表只保留常用地址,这样订单独立成档,不受地址表修改影响;order_item里存了product_name和product_cover快照,商品后来改名或换图,历史订单里的显示不会被带偏,这是答辩时能拿出来讲的数据冗余理由。
2.3 字段类型与通用坑
这类毕设项目最常见的三类坑,在建表时就能避开。金额一律用decimal(10,2),绝对不要用double,浮点运算在累计金额时会差出分单位;时间字段统一用datetime,Java 端对应LocalDateTime,配合 MyBatis-Plus 的自动填充,省掉手写setCreateTime;商品删除用deleted字段做逻辑删除,而不是DELETE FROM product,电商项目最忌讳物理删数据,留下痕迹在答辩里就是加分项。
状态字段也要养成写注释的习惯。比如商品状态0下架 1上架 2待审核、订单状态0待付款等,数据库里注释写清楚,后面写switch或者前端枚举判断时不用再猜。分类表可以预置几条数据:水果、蔬菜、粮油、禽蛋,农户上架商品时从下拉里选,不让农户手输分类,数据质量会干净很多。
3. 后端实现:SpringBoot 接口设计与关键代码
后端部分,我按一个标准的前后端分离项目来拆。这套接口设计不只是“能跑”,还要让答辩老师觉得你是理解工程化的,而不是把代码堆在一个 Controller 里。
3.1 服务端分层与包结构
我常用的分层是controller / service / mapper / entity / common / config。Controller 只做参数接收和结果包装,Service 写业务逻辑,Mapper 只碰 SQL。商品、订单、用户这些模块各自建一个包,避免出现十来个 Controller 塞在同一个目录下的情况。
src/main/java/com/qianhong/platform/ ├── controller/ # 接口入口,只做参数解析与结果返回 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis-Plus 的 Mapper 接口 ├── entity/ # 数据库表对应的实体类 ├── common/ # Result、异常、常量、工具类 └── config/ # JWT、MyBatis-Plus、静态资源等配置分层清晰的项目,后端翻代码时只要看 Service 就能定位业务逻辑。比如下单接口,Controller 只有两行,真正的库存扣减、订单生成都在OrderServiceImpl里,导师问起“异常了怎么办”,你直接说@Transactional加在实现类方法上就行。
3.2 登录鉴权:JWT + 拦截器
这个体量的项目上 Spring Security 太重,配置类一大堆,调试成本高。我一般用 Hutool 的 JWT 工具做登录鉴权,配合一个 HandlerInterceptor,代码量小,讲解也直观。先写一个 JWT 工具类:
public class JwtHelper { private static final byte[] KEY = "qi-nong-platform".getBytes(); public static String createToken(Long userId, Integer role) { return JWT.create() .setKey(KEY) .setPayload("userId", userId) .setPayload("role", role) .setExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L)) .sign(); } public static JWT parseToken(String token) { JWT jwt = JWTUtil.parseToken(token); if (!jwt.setKey(KEY).verify()) { throw new BusinessException(401, "token 校验失败"); } return jwt; } }createToken里设置了 7 天有效期,毕设演示期间基本够用;KEY是签名密钥,源码包里如果给了配置项,上线时一定要换成环境变量,别把密钥硬编码提交到仓库。parseToken里先解析再校验签名,校验失败统一抛 401,由全局异常处理器转成 JSON 返回。
登录接口和拦截器这样配合:
@RestController @RequestMapping("/api/auth") public class AuthController { @Resource private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } String token = JwtHelper.createToken(user.getId(), user.getRole()); return Result.ok().put("token", token).put("userInfo", user); } }@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (StrUtil.isBlank(token)) { throw new BusinessException(401, "未登录"); } JWT jwt = JwtHelper.parseToken(token); request.setAttribute("userId", Long.valueOf(jwt.getPayload("userId").toString())); request.setAttribute("role", jwt.getPayload("role")); return true; } }@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/**", "/api/product/list"); } }拦截器里把userId和role放进了 request 属性,Controller 里直接(Long) request.getAttribute("userId")拿当前用户,不用每个接口再解析一次 token。excludePathPatterns放行登录接口和商品列表,这两个是所有用户都能访问的,其余接口默认都要 token。需要按角色控制的接口,比如农户发布商品,在 Controller 里加一行角色判断,或者用自定义注解,效果都比想象中好讲。
3.3 商品列表分页与条件查询
商品列表是前端首页的主力接口,涉及分页和条件筛选。MyBatis-Plus 的分页插件在这个场景里几乎是标配,先注册插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }注意:如果你用的是 SpringBoot 3.x,要引入mybatis-plus-spring-boot3-starter而不是老的mybatis-plus-boot-starter,否则启动会直接报 Bean 找不到。这是版本升级后最经典的一个坑。
分页查询代码:
@Override public Page<Product> pageProducts(int pageNum, int pageSize, String keyword, Long categoryId) { Page<Product> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>() .eq(Product::getStatus, 1) .like(StrUtil.isNotBlank(keyword), Product::getName, keyword) .eq(categoryId != null, Product::getCategoryId, categoryId) .orderByDesc(Product::getCreateTime); return productMapper.selectPage(page, wrapper); }pageNum从 1 开始,pageSize默认 10,前端传参时我用current和size做映射,避免语义混乱。LambdaQueryWrapper的like条件带上了keyword是否为空判断,为空时该条件不拼进 SQL,比手动拼字符串安全得多,也天然防住了模糊查询的注入风险。status = 1写死为“上架”,保证前台用户永远看不到审核中和下架商品。
3.4 下单流程与库存扣减
下单是电商项目的核心事务。我推荐一个既简单又能讲清楚的方案:@Transactional保证订单、明细、库存扣减要么全部成功要么全部回滚,库存扣减用条件更新,不先查询再更新,从源头避开超卖问题。
@Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto, Long userId) { String orderNo = "QH" + System.currentTimeMillis() + RandomUtil.randomNumbers(4); OrderInfo order = new OrderInfo(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(0); order.setTotalAmount(dto.getTotalAmount()); order.setReceiverName(dto.getReceiverName()); order.setReceiverPhone(dto.getReceiverPhone()); order.setReceiverAddress(dto.getReceiverAddress()); orderMapper.insert(order); for (OrderItemDTO item : dto.getItems()) { int rows = productMapper.deductStock(item.getProductId(), item.getCount()); if (rows == 0) { throw new BusinessException("商品库存不足或已下架"); } OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setProductName(item.getProductName()); orderItem.setProductCover(item.getProductCover()); orderItem.setPrice(item.getPrice()); orderItem.setCount(item.getCount()); orderItemMapper.insert(orderItem); } return order.getId(); }@Update("UPDATE product SET stock = stock - #{count} WHERE id = #{productId} AND stock >= #{count} AND status = 1") int deductStock(@Param("productId") Long productId, @Param("count") int count);这里重点说两个细节。订单号用“前缀 + 时间戳 + 4 位随机数”,比 UUID 短得多,用户和后台都愿意看;扣库存的 SQL 带了stock >= #{count},数据库层面保证扣减后库存不为负,如果这条 SQL 影响行数为 0,说明库存不足或者商品已下架,业务直接抛异常。@Transactional(rollbackFor = Exception.class)一定要写rollbackFor,否则遇到非受检异常可能不回滚。这个知识点在面试里大概率被问到“数据库怎么保证数据一致性”,这条 SQL 就是最直接的答案。
4. 前端实现:Vue 路由、状态与接口联调
前端我用 Vue3 + Vite 的写法来拆。源码包里可能是 Vue CLI 创建的老结构,但页面组件和路由逻辑是相通的,核心思路能直接平移。
4.1 环境准备与项目初始化
先把 Node 环境切到 18 LTS,这是 Vue3 生态兼容性最好的版本。Node 20 以上跑老项目偶尔会报 OpenSSL 相关的 hash 错误,降到 18 立刻正常。依赖安装顺序:
nvm use 18 cd 前端项目目录 npm install npm install element-plus axios pinia vue-router@4 npm run develement-plus负责 UI 组件,axios做 HTTP 请求,pinia管理购物车、用户信息这类跨页面状态,vue-router@4是 Vue3 对应的路由版本。如果源码包里自带node_modules,我一般直接删掉重装,因为本机 Node 版本和依赖锁定版本对不上时,报错很难定位。
4.2 路由配置与页面结构
页面和路由的对应关系如下:
| 路由 | 页面组件 | 说明 |
|---|---|---|
/home | Home.vue | 首页商品列表 |
/product/:id | ProductDetail.vue | 商品详情 |
/cart | Cart.vue | 购物车 |
/orders | OrderList.vue | 我的订单 |
/login | Login.vue | 登录 |
/admin/goods | AdminGoods.vue | 管理员/农户商品管理 |
路由配置加上登录守卫和角色判断:
const routes = [ { path: '/home', component: Home, meta: { title: '首页' } }, { path: '/product/:id', component: ProductDetail }, { path: '/cart', component: Cart, meta: { requiresAuth: true } }, { path: '/admin/goods', component: AdminGoods, meta: { requiresAuth: true, role: 0 } }, { path: '/login', component: Login } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role !== undefined && to.meta.role !== Number(localStorage.getItem('role'))) { next('/home') } else { next() } })这里把本地存储的role和路由meta.role做比对,管理员后台页面普通人进不去。调试路由跳转和守卫逻辑时,Vue DevTools 插件要看一眼路由状态变化,比在控制台瞎猜强得多。我用的是createWebHistory()历史模式,URL 干净,但部署后刷新会踩 404 的坑,这个在第 5 章展开。
4.3 Axios 封装与登录状态处理
前端所有接口请求建议走一个统一封装的 Axios 实例。开发环境用 Vite 代理把/api转发到后端,生产环境用 Nginx 转发,前端代码里只写相对路径。
const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( res => { if (res.data.code === 200) return res.data if (res.data.code === 401) { localStorage.removeItem('token') localStorage.removeItem('role') router.push('/login') return Promise.reject(new Error('登录已过期')) } return Promise.reject(new Error(res.data.msg)) }, err => { return Promise.reject(err) } )token 我用localStorage存而不是sessionStorage,因为用户刷新页面后登录状态不能丢。baseURL: '/api'是相对路径,开发环境在vite.config.js里配 proxy 转发到http://localhost:8080,这样前端写代码时不需要关心后端地址,换环境只改代理配置。响应拦截器里对 401 统一处理,清掉本地状态并跳到登录页,这是第 5 章会说到的“接口报 401 页面却没反应”的正确解法。
4.4 商品详情与购物车交互
商品详情页要接收路由参数,Vue Router 的用法是:
import { useRoute } from 'vue-router' const route = useRoute() const productId = Number(route.params.id) onMounted(() => { loadProductDetail(productId) })注意route.params.id拿到的类型是字符串,请求后端前先转成 Number,否则某些后端框架在类型转换上会出幺蛾子。广告位不想要可以用 div 做平替:
购物车状态我推荐用 Pinia 管理。前后端交互上,购物车建议走后端接口,而不是只存在前端内存里:用户加购、改数量、删除都调用后端接口,刷新页面后购物车数据还在。前端只保存一个cartCount用于角标展示:
import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ count: 0 }), actions: { async fetchCartCount() { const res = await request.get('/api/cart/count') this.count = res.data } } })这样一个 store 就能支撑整个项目的购物车角标。结算时把购物车里勾选的商品 ID 列表传给后端下单接口,后端统一去查最新价格和库存,避免前端传过来的价格被篡改。这个点答辩时主动说出来,老师会认为你考虑到了“价格以服务端为准”的安全意识。
5. 避坑与排查:前后端分离项目最常见的五个翻车点
这一章写的是我做这类项目反复踩过的坑,每条都按“现象 → 原因 → 解决”说清楚,能省下很多联调时间。
5.1 跨域请求被拦截
现象:前端npm run dev启动在 5173 端口,后端启动在 8080 端口,浏览器控制台报 CORS error,接口数据进不来。
原因:前后端分离项目,两个端口不是同源,浏览器默认拦截跨域 AJAX 请求。很多人一上来就在后端加@CrossOrigin,改来改去还是不稳定。
解决:开发环境用 Vite 代理解决,这是最干净的方式。在vite.config.js里配置:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }前端请求/api/product/list,Vite 在开发服务器层面转发到http://localhost:8080/api/product/list,浏览器看到的是同源请求,根本不会触发跨域。生产环境用 Nginx 反向代理做同样的事。后端可以保留 CORS 配置兜底,但不要依赖它。
5.2 商品图片上传成功却访问不到
现象:农户在后台传图片,接口返回成功,数据库里也存了路径,但前端img.src拼上路径后图片 404。
原因:图片保存到了本地磁盘目录,比如D:/upload/xx.jpg,但 SpringBoot 默认只映射classpath:/static/,访问/upload/xx.jpg时根本没有对应处理器。
解决:在 WebConfig 里加静态资源映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadDir); }uploadDir是图片存放的磁盘绝对路径。注意拼接时file:后要跟带斜杠的绝对路径,Windows 和 Linux 写法不同,这是最容易漏的地方。更进一步的做法是上 MinIO 做对象存储,接口逻辑不变,只是把落盘换成客户端;毕设阶段先把本地映射配好就能跑通展示。
5.3 登录状态失效后页面没跳转
现象:用户在那放着不动,过了 7 天 token 过期,再点“提交订单”接口报 401,但页面没反应,也没有跳回登录页。
原因:后端返回了 401,但前端 Axios 响应拦截器里没有统一处理 401,每个页面都要自己写判断,漏一个接口就漏一个跳转。
解决:按 4.3 的做法,在 Axios 响应拦截器里统一判断code === 401,清掉 token 和用户信息,然后router.push('/login')。这样全项目只需要写一次。还有一个细节:跳转登录页时要携带当前路由地址,登录成功后回跳,体验会好很多。
5.4 数据库时间比本地早 8 小时
现象:后台添加商品,列表里显示的create_time比北京时间早了 8 个小时。
原因:MySQL 连接串里的serverTimezone没设置,驱动用了默认 UTC 时区。数据库本地时间没问题,但 Java 拿到的值在序列化输出时被当成了 UTC。
解决:数据源连接串加上时区参数:
jdbc:mysql://localhost:3306/qi_nong?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai同时配置 Jackson 统一时间格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这两处同时改完,前后端显示的时间就一致了。这类时区问题最玄学,因为数据库和 Java 进程本地看着都没问题,只有拼在一起才暴露。
5.5 打包部署后刷新子页面 404
现象:前端npm run build后部署到 Nginx,访问首页正常,但刷新/product/1或/orders页面时变成 404。
原因:前端是 Vue Router 的createWebHistory()模式,路由是前端 JS 控制的,Nginx 里没有对应/product/1这个真实文件,请求落到了静态目录上找不到文件。
解决:Nginx 配置里加一条 try_files:
location / { try_files $uri $uri/ /index.html; }try_files的含义是:先找$uri对应的文件,找不到就找目录,再找不到直接回退到index.html,由 Vue Router 接管路由。加了这一行,刷新任何子路由都能正常回到前端路由逻辑。如果你不想碰 Nginx 配置,也可以把路由模式改成createWebHashHistory(),URL 里带#号,部署就永远不会 404,但 URL 不美观,看你的取舍。
6. 部署验证与排查技巧:从本机跑通到上线可演示
项目开发完到能演示,中间还差一次正经的部署。这一章的步骤走完,答辩演示时才能稳定不翻车。
6.1 打包顺序与上线检查清单
后端和前端各自打包:
mvn clean package -DskipTestsnpm run build后端产出一个jar包,前端产出一个dist目录。部署到服务器时,两个产物分别放好,前端dist交给 Nginx,后端jar用java -jar启动。我每次上线前强制走一遍这个清单:
- 确认后端
application-prod.yml里的数据库地址、账号密码是生产环境的,不是本机的 localhost。 - 确认服务器 MySQL 里执行过初始化 SQL,表结构和预置数据都在。
- 确认 Nginx 的
/api代理指向后端实际端口。 - 启动后先
curl http://localhost:8080/api/auth/login验证后端口活通,再去浏览器刷新前端页面。 - 顺手把 SpringBoot 默认的 Banner 换成项目 LOGO,终端启动时一眼能看到服务起来了,这个细节答辩现场印象很深。
6.2 一个排查技巧:把 jar 当目录看
上线后如果接口行为不对劲,不要急着反编译整个项目。jar 就是个 zip,先把它当目录列出来看:
jar tf qianhong-platform.jar | grep applicationunzip -p qianhong-platform.jar BOOT-INF/classes/application-prod.ymljar tf列出 jar 里所有文件,grep application过滤出配置文件;unzip -p直接把文件内容打印到终端,不用解压、不用反编译。我遇到过好几次,本地跑得好好的,一上线就报数据库连不上,查了一圈发现是application-prod.yml里数据源配的还是内网地址。用这两条命令,10 秒就能定位是不是打进了错误的配置。这个技巧比用 IDE 反编译整个 jar 快得多,排查线上问题时非常实用。
从那以后,我每次部署完都强制自己走一遍这个顺序:先看 jar 里的配置,再 curl 后端接口,最后刷新前端页面。做完这个项目,JWT 鉴权、分页插件、事务回滚、跨域处理这几个点,正好就是 Java 面试题里最常被翻牌子的内容,把代码里每一处都吃透,比背八股文踏实得多。希望帮到你。
本文还有配套的精品资源,点击获取