简介:面向毕业设计场景的家具电商网站完整源码包,基于Spring Boot、Vue.js和MySQL开发,适合Java方向学生用于毕设参考或二次开发。项目采用前后端分离结构:前端Vue负责页面交互与路由,后端Spring Boot提供RESTful接口,MySQL存储家具商品、会员与订单数据。业务上实现家具分类展示、会员查询、在线选购下单,管理员可上架、下架商品并统计销售订单,覆盖电商后台常见流程。包内附带毕业论文文档及项目说明,便于直接对照撰写,也可在此基础上扩展功能模块。压缩包共802个文件,容量约20.05MB,以122个Java后端类、46个Vue组件、153个JavaScript脚本及SVG图片资源为主,同时包含SQL数据库脚本、Maven配置、YAML/Properties配置和bat一键启动脚本,结构清晰便于导入IDE运行。已有105人浏览学习,适合毕业设计选题或想快速上手Spring Boot+Vue整合开发的学习者。
1. 当你在搜索框敲下"Spring Boot + Vue 家具网站 源码 带毕业论文"时,想找的往往不是又一个增删改查 demo,而是一条能完整串起前端页面、后端接口、数据库表和论文文档的学习主线。家具网站这个选题的优势在于业务模型足够收敛——商品、分类、购物车、订单就是全部核心,没有秒杀、优惠券、直播这类营销逻辑来干扰你理解全栈架构。这套源码适合两类人:准备毕业设计答辩的学生,需要一份能讲清楚每个表、每个接口、每个页面存在理由的完整项目;以及刚掌握 Vue 基础、想看看真实项目中前后端如何对接的开发者。下面按后端、前端、业务链路和部署验证的顺序,把这类源码会涉及的架构选择、参数配置和坑位一次讲完。
2. Spring Boot 后端:四层架构与目录规范撑起整套接口
2.1 拿到源码先看包结构:Controller、Service、Mapper、Entity 四层定位
毕业设计级的家具网站后端,不管底层是 MyBatis 还是 JPA,包结构几乎都约定成四层:Controller 负责接收 HTTP 请求和参数校验;Service 负责业务规则与事务边界;Mapper/Repository 负责数据库访问;Entity 跟表结构一一对应。四层的引用方向是单向的,Controller 依赖 Service,Service 依赖 Mapper,写页面逻辑的地方永远不直接碰数据库。
我拿到一份源码后的第一个动作,不是急着跑起来,而是打开src/main/java下的包名列表:
com.example.furniture ├── controller # REST 接口入口,一个实体对应一个 Controller ├── service # 业务层,事务注解和核心规则放在这里 ├── mapper # MyBatis 场景叫 mapper,JPA 场景叫 repository ├── entity # 和表字段一一对应的实体类 ├── config # 跨域、静态资源映射、拦截器配置 ├── common # Result 返回体、全局异常、JWT 工具类 └── FurnitureApplication.java这个目录结构不是随便分的。比如common/Result.java的出现,基本能判断作者做的是前后端分离设计而不是模板渲染。因为所有接口返回统一结构时,前端才能用一个 axios 拦截器处理所有错误分支。如果你的 Controller 直接返回实体对象或者返回Map,说明接口风格不统一,前端代码里一定会出现大量重复判空,这类项目在论文答辩时也容易暴露设计功底不足。
2.2 JPA 还是 MyBatis:家具网站源码里最常出现的两种数据层
判定一套源码用哪个数据层框架,打开pom.xml看依赖就能确定。两者没有绝对优劣,但教育类项目用 JPA 的比例更高,因为单表 CRUD 代码量少;而带复杂联表查询的项目更偏向 MyBatis。实际区别可以看这张对比:
| 对比项 | Spring Data JPA | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 继承 JpaRepository 直接可用 | IService 接口开箱即用 |
| 复杂多表查询 | @Query 写 JPQL 或原生 SQL | XML resultMap 或注解 SQL |
| 分页 | Pageable 对象 | Page 对象 + 分页插件 |
| 学习成本 | 需要理解方法名解析和持久化上下文 | 需要会写 SQL 和 resultMap |
家具网站最典型的联表场景是:列表页要展示商品,同时需要展示商品所属的分类名。如果商品表里只有categoryId,用 JPA 惯用的做法是在实体上声明@ManyToOne关联:
@Entity @Table(name = "furniture") public class Furniture { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; private String image; private Integer stock; private String description; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") @JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "furnitureList"}) private Category category; }这段代码有三个容易踩的点。第一,fetch = FetchType.LAZY表示查商品时不马上查分类,但懒加载要求在事务范围内操作,如果 Service 层方法没加@Transactional,Jackson 在向前端序列化时访问getCategory()就会抛LazyInitializationException。第二,@JsonIgnoreProperties里必须带上hibernateLazyInitializer和handler,这是 Hibernate 代理对象自带的字段,不忽略会直接序列化失败。第三,如果Category里也声明了回引字段furnitureList,不加这个忽略项就会变成双向递归,返回 JSON 时直接堆栈溢出。
用 MyBatis 时写法不同,在 XML 里用<association>标签映射分类对象:
<resultMap id="FurnitureWithCategory" type="com.example.furniture.entity.Furniture"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="price" property="price"/> <association property="category" javaType="com.example.furniture.entity.Category"> <id column="cid" property="id"/> <result column="category_name" property="name"/> </association> </resultMap>association解决的是多对一关系,意思是“一条商品记录对应一条分类记录”。列别名category_name来自联表查询 SQL 里的c.name as category_name,cid则是c.id as cid。这样前端拿到商品列表时,每个商品对象里已经嵌套好了分类名,不用再发第二趟请求查分类。
2.3 Controller 统一返回体与全局异常处理
前后端分离项目的 Controller 不应该直接返回实体对象,否则前端每次都要判断“返回的是数据还是异常页”。常见方案是所有接口返回Result<T>:
@RestController @RequestMapping("/api/furniture") public class FurnitureController { @GetMapping("/list") public Result<Page<Furniture>> list(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "8") int size) { Page<Furniture> data = furnitureService.pageList(page, size); return Result.success(data); } }defaultValue = "1"和defaultValue = "8"的意义是前端不传分页参数时接口也能跑,首页默认展示 8 件商品。Controller 层不要写try-catch,数据异常、空指针异常都抛给统一的@RestControllerAdvice处理,前端能拿到格式一致的错误消息。这个设计在论文“系统设计”部分可以画一张异常处理流程图,算是答辩时能主动讲出来的点。
2.4 JWT 登录鉴权:过滤器里的放行路径与 token 前缀
用户模块一般是注册、登录、个人信息修改。登录接口校验完用户名密码后签发一个 JWT 返回前端,前端把 token 存进 localStorage,在 axios 请求头Authorization: Bearer <token>中回传。后端用一个过滤器统一鉴权:
@Component public class JwtAuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String uri = request.getRequestURI(); // 匿名可以访问的接口放行,避免首页商品展示也必须登录 if (uri.startsWith("/api/user/login") || uri.startsWith("/api/furniture") || uri.startsWith("/api/category")) { chain.doFilter(request, response); return; } String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { Claims claims = JwtUtil.parseToken(header.substring(7)); request.setAttribute("userId", claims.get("userId")); chain.doFilter(request, response); } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } } }两个高频报错点。第一,header.substring(7)的前提是客户端确实加了Bearer前缀,而且 Bearer 首字母大写。如果前端 axios 拦截器注入的是'Bearer ' + token,这里就能对上;如果两端前缀规则不一致,过滤器解析出来是整串乱码,JWT 解析必然失败。第二,放行名单要跟前端路由守卫对齐,购物车、订单这类接口必须登录,否则未登录用户打开详情页没问题,但一点“加入购物车”就跳 401。
项目里如果引入了spring-boot-starter-actuator,默认会暴露/actuator/env等敏感端点,未授权访问可能直接把数据库配置暴露出去。生产环境至少要把暴露范围收缩到健康检查:
management: endpoints: web: exposure: include: health,info注意:这行配置只保留
health和info两个端点,排查线上接口问题时再临时放开,用完立即改回去。
3. Vue 前端:环境配置、路由参数与 axios 拦截器把页面跑通
3.1 第一次跑起来:node 版本、npm 安装与 devServer 转发
“Vue 安装及环境配置”是这类源码阅读中最容易被卡住的一环。拿到源码先看package.json里的 scripts 和依赖版本:Vue 2 配 vue-cli 时,启动脚本一般是npm run serve;Vue 3 配 Vite 时一般是npm run dev。如果按 README 命令执行后报vite: not found,通常是依赖没装全或 node 版本太老。
node -v npm -v # 如果安装到一半失败,把 node_modules 删干净再重来,避免残留半成品依赖 rm -rf node_modules package-lock.json npm install --registry=https://registry.npmmirror.comnpm install有两个常见问题。ERESOLVE依赖冲突,多半是 Vite 与旧版 vue-router 的 peer dependency 冲突,可以临时用npm install --legacy-peer-deps绕过;Node Sass does not yet support说明源码用的是 node-sass 编译方案,建议直接把依赖换成sass(Dart Sass)并调整vue.config.js里的 css 配置,新版 Node 对 node-sass 的兼容性越来越差。
启动后开发服务器端口是 5173(Vue3 + Vite)或 8080(Vue2 + vue-cli),后端接口在 8080 或 8081,端口不固定时最稳的联调方式是在前端工程里配转发:
// vite.config.js export default { server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }配置之后,前端请求写的都是相对路径/api/furniture/list,浏览器里始终是同源请求,不会触发跨域。代码部署到生产环境时,只需要在 nginx 里把/api同样转发给后端 jar,前端代码一行都不用改。用了这个方案,后端就不要再开addCorsMappings全放行,两套处理叠加起来,个别浏览器上肯会出现重复响应头的兼容性报错。
3.2 商品详情页路由:params 与 query 两种传参方式
家具网站最基础的路由表包含首页、分类页、详情页、购物车、订单列表和个人中心。详情页一般用动态路径/furniture/:id:
// router/index.js const routes = [ { path: '/', component: HomeView }, { path: '/category/:id', component: CategoryView }, { path: '/furniture/:id', name: 'Detail', component: DetailView }, { path: '/cart', component: CartView }, { path: '/login', component: LoginView } ]列表页跳详情页和详情页取参,有两种常见写法:
| 传参方式 | 跳转代码 | 接收方式 | 刷新后表现 |
|---|---|---|---|
| params | router.push({ name: 'Detail', params: { id: 1 } }) | route.params.id | 刷新后参数丢失 |
| query | router.push({ path: '/furniture/' + id }) | route.params.id | 刷新后参数保留 |
params 丢参数的原因:Vue Router 4 里只有路径上声明:id的字段才能进入params。如果跳转时写的是{ path: '/furniture', params: { id: 1 } },路径里没有:id占位,参数不会编译进 URL,刷新页面之后params就是空对象。所以只要路由路径写了:id,直接用字符串拼接最稳:
goDetail(id) { this.$router.push(`/furniture/${id}`) }详情页拿到id之后再请求/api/furniture/${id}取商品详情。这里有个面试高频考点:同一个组件复用时,created钩子不会重复触发。从沙发详情页跳转到餐桌详情页,DetailView组件会被复用,此时要在watch里监听$route.params.id的变化再重新请求,否则页面显示的永远是上一个商品。
3.3 路由守卫和 axios 拦截器:登录状态的两道关卡
前端路由守卫是第一道关卡,拦截未登录用户访问购物车、订单页:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const requiresAuth = to.matched.some(record => record.meta.requiresAuth) if (requiresAuth && !token) { next('/login') } else { next() } })配套地,在路由定义里给需要登录的页面加meta: { requiresAuth: true }。路由守卫只能控制页面能不能打开,真正校验 token 有效性还得靠后端接口,所以 axios 拦截器是第二道关卡:
// src/utils/request.js import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 8000 }) // 请求拦截器:每次请求自动携带 token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) // 响应拦截器:统一处理 401 request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )response => response.data这一步把 axios 的外层剥掉,组件里res拿到的就是{ code, message, data },不用每次写两层.data.data。timeout: 8000表示接口超过 8 秒未响应就主动断开,前端给出超时提示,而不是让页面一直转圈。
3.4 Vuex 管用户态还是管购物车
家具网站前端的全局状态通常只涉及用户信息和购物车数量。常见源码的做法是:用户信息放 Vuex,购物车明细放 localStorage,用 Vuex 里的cartCount做角标展示。登录成功后提交 mutation 保存用户态:
this.$store.commit('setUser', res.data) localStorage.setItem('token', res.data.token)购物车放 localStorage 的好处是未登录用户也能把商品加进购物车,后端不需要为匿名用户单独设计一套临时购物车表。等用户真正登录后,再把本地购物车合并到后端,这一步就是论文“购物车同步功能”的原型。
4. 家具商品展示与搜索:分类、轮播图、图片和搜索防抖
4.1 分类导航的数据来源:接口返回树还是前端自己组装
家具网站的分类导航一般放在首页顶部或侧边栏,数据来自category表。这张表最简设计只需要id、name、parent_id、sort_order四个字段。parent_id = 0表示一级分类,其余表示挂在某个一级分类下的子分类。
后端接口有两种返回方式。第一种是返回完整列表,前端根据parent_id自己组装树:
buildTree(list) { const map = {} list.forEach(item => { map[item.id] = { ...item, children: [] } }) const tree = [] list.forEach(item => { if (item.parentId === 0) { tree.push(map[item.id]) } else if (map[item.parentId]) { map[item.parentId].children.push(map[item.id]) } }) return tree }第二种是后端直接返回组装好的树形 JSON,前端拿到直接渲染。两种做法在后端代码量上差别不大,但前端组装方案的好处是分类调整后不用改接口,后端返回树的好处是前端组件代码更少。毕业设计建议选前端组装,答辩时可以讲“树的组装逻辑放在前端,后端保持接口简单”。
4.2 搜索接口与防抖:后端 LIKE 查询 + 前端 setTimeout
搜索是家具网站信息检索的核心入口。后端接口接收keyword参数,JPA 里用findByNameContaining(keyword)实现LIKE '%keyword%'。这个方法名解析规则是:Containing等价于 SQL 的LIKE %值%。要注意的是%keyword%这种写法带有前导通配符,无法命中普通 B-tree 索引,数据量一旦涨到十万级就会明显变慢。对毕业设计几百条家具数据不是问题,但论文“性能分析”里最好补一句“后续可引入全文索引”。
前端真正的坑是“每敲一个字符就发一次请求”。搜索框的input事件在输入“沙”和“沙发”之间会触发两次请求,不加防抖,后端会被短暂打满无效查询:
export default { data() { return { keyword: '', timer: null } }, methods: { onInput() { // 每次输入先清掉上一个定时器,300ms 内没有新输入才真正发起请求 clearTimeout(this.timer) this.timer = setTimeout(() => { this.fetchList() }, 300) }, async fetchList() { const res = await this.$axios.get('/furniture/search', { params: { keyword: this.keyword.trim() } }) this.list = res.data } } }clearTimeout + setTimeout就是软件防抖,300ms 是一个比较稳的阈值:太短防抖失去意义,太长用户会感觉列表刷新被拖慢。提交给后端的keyword一定要先trim(),避免用户敲了个空格也当成搜索词。如果源码里用的lodash,可以直接写this.debouncedFetch = _.debounce(this.fetchList, 300),原理完全一样。
4.3 轮播图和商品图片的加载链路
首页轮播图的数据表一般是banner,字段包含id、image_url、link_url、sort_order。商品图片表和家具表放在一起,image字段存相对路径,比如/upload/sofa/1.jpg。关键问题在于 Spring Boot 默认只映射classpath:/static/下的静态资源,上传到本地磁盘的图片访问不到,因此必须加一个资源映射配置:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /upload/** 路径映射到本机磁盘目录 registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/upload/"); } }这段配置在 Windows 和 Linux 上路径不同。源码里如果写死D:/upload/,部署到服务器上会 404。稳妥做法是路径写进application.yml:
upload: path: /data/furniture/upload/然后再用@Value("${upload.path}")注入到配置类里。前端图片加载失败时要有兜底,否则页面渲染出一堆碎图:
<img :src="item.image" @error="handleImgError" />methods: { handleImgError(e) { // 网络慢或图片被删时,换成项目内置占位图 e.target.src = require('@/assets/fallback.jpg') } }4.4 如果源码里要用视频宣传家具:m3u8 播放的处理方式
家具网站的项目展示页有时会放一段宣传视频,标题描述“vue播放m3u8”指的就是 HLS 流媒体的处理。浏览器原生<video>标签并不能直接播放 m3u8,需要借助 hls.js:
import Hls from 'hls.js' export default { mounted() { const video = this.$refs.video const videoUrl = 'https://example.com/video/livingroom.m3u8' if (Hls.isSupported()) { const hls = new Hls() hls.loadSource(videoUrl) hls.attachMedia(video) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { // iOS Safari 原生支持 HLS video.src = videoUrl } } }Hls.isSupported()判断当前浏览器是否支持 MSE,支持就走 hls.js 拉流;不支持时回退到原生canPlayType。这个方案的关键点在于视频地址要交给 hls.js 处理,而不是直接填到<video src>里,否则大部分桌面浏览器都会黑屏。
5. 购物车、订单与支付状态:交易链路的核心逻辑
5.1 购物车的两种存储方案:前端 localStorage 还是后端表
家具网站的购物车模块有两种常见设计。第一种是未登录用户把商品信息存进 localStorage,登录后调接口同步到后端。第二种是强依赖登录,购物的每一步都走接口。下表是两种方案在毕业设计里的取舍:
| 对比项 | 前端 localStorage | 后端购物车表 |
|---|---|---|
| 未登录体验 | 可以先加购,后登录 | 必须登录才能加购 |
| 多端同步 | 不支持,换设备就丢 | 支持 |
| 后端开发量 | 少,只要一个同步接口 | 多一整套增删改查 |
| 论文可写内容 | “购物车合并策略” | “基于表结构的购物车设计” |
我一般建议做 localStorage + 后端同步方案。前端购物车的数据结构是一组{ furnitureId, count, selected },提交订单时再带上商品价格,后端重新计算总价。登录后把本地数组合并到后端表的 SQL 用ON DUPLICATE KEY UPDATE count = count + VALUES(count)(MySQL 语法)即可,这个点在论文里可以单独讲实现思路。
5.2 订单表设计:快照字段与库存扣减的先后顺序
订单模块是家具网站的核心,数据上分为订单主表和订单明细表。主表的常用字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单号,前端展示用 |
| user_id | bigint | 下单用户 |
| total_amount | decimal(10,2) | 总金额 |
| status | tinyint | 0待支付 1已支付 2已发货 3已完成 4已取消 |
| receiver_name | varchar(50) | 收货人 |
| receiver_phone | varchar(20) | 收货电话 |
| receiver_address | varchar(255) | 收货地址 |
| create_time | datetime | 下单时间 |
| pay_time | datetime | 支付时间 |
明细表里必须冗余存一份商品名称、商品快照价格和商品图片。原因在于后端Furniture表的字段修改后,历史订单要保留下单那一刻的商品信息,这就是电商里常说的“快照字段”。如果不存快照,家具涨价之后,半年前的订单里显示的金额就跟界面不一致,数据分析时也说不清当时的实际售价。
创建订单时,库存扣减和订单插入要放在同一个事务里:
@Transactional public Order createOrder(Long userId, List<CartItem> items, Address address) { // 1. 遍历购物车,逐件扣库存 for (CartItem item : items) { int updated = furnitureMapper.deductStock(item.getFurnitureId(), item.getCount()); if (updated == 0) { throw new BusinessException("商品库存不足:" + item.getFurnitureName()); } } // 2. 汇总金额,插入订单主表和明细表 // 3. 清空已下单的购物车项 return order; }扣库存的 SQL 里必须加上库存充足条件,否则会出现并发超卖:
UPDATE furniture SET stock = stock - #{count} WHERE id = #{furnitureId} AND stock >= #{count}stock >= #{count}这个条件让数据库在原子操作层面避免负库存。更新行数为 0 时说明库存不够,抛异常回滚整个订单,保证商品和库存永远一致。
5.3 订单状态机:防止状态乱跳的代码写法
订单状态从 0 到 4,不是随便哪个状态都能直接跳过去。不合理的跳转有:待支付订单直接变成已完成、已取消订单重新变成已支付。因此后端在每次状态变更时要校验当前状态是否符合流转规则:
public void cancelOrder(Long orderId, Long userId) { Order order = orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException("订单不存在")); // 校验操作者必须是订单所属用户 if (!order.getUserId().equals(userId)) { throw new BusinessException("无权限操作此订单"); } // 只有待支付状态才能取消 if (order.getStatus() != 0) { throw new BusinessException("当前状态不可取消"); } order.setStatus(4); order.setCancelTime(new Date()); orderRepository.save(order); }这段逻辑看起来简单,但它是确保状态一致性的核心防线。前端再怎么隐藏按钮,后端不校验就会有人绕过界面直接调接口把订单状态打乱。论文里画一张状态流转图,并把每个状态对应的操作接口列成表格,属于答辩时最能体现软件工程意识的模块之一。
5.4 并发下单的库存超卖与幂等控制
家具网站虽然不会有秒杀级别的并发量,但“同一把椅子两个人同时下单”在并发上会让库存减出负数。上面的stock >= #{count}在单条 SQL 层面是安全的,另外还要处理“同一用户重复提交订单”的情况。常见的辅助方案是幂等号:前端在创建订单时生成一个token(或基于userId + timestamp计算),后端在事务内先去幂等表查这个 token 是否已存在,存在就直接返回已有订单,不存在才插单。
另一种做法是给订单表加user_id + order_no唯一索引,重复插入时数据库直接抛异常。毕业设计里这两种选一种即可,论文“系统测试”部分可以造两个线程并发提交订单,用压测对比扣库存前后的字段变化。
6. 部署与验证:前端打包后的布局异常和后端 jar 启动的排查路径
6.1 前端打包后布局异常的常见来源与排查顺序
开发环境页面正常,npm run build之后传到服务器却出现样式错乱、图片 404,这是 Vue 项目最典型的部署问题。按下面的顺序排查:
第一步,看浏览器 Network 面板里 CSS/JS 文件的加载路径。如果文件挂在http://your-domain/furniture/css/app.css,而vite.config.js里没设置base,资源会请求到域名根路径,自然 404。Vue 3 + Vite 项目打包前先确认 base:
export default defineConfig({ base: process.env.NODE_ENV === 'production' ? '/furniture/' : '/' })第二步,看路由模式。如果用的createWebHistory,部署到非根目录时刷新详情页会 404,因为 nginx 找不到对应的物理路径。把路由改成createWebHashHistory或在 nginx 配try_files $uri $uri/ /index.html;二选一。
第三步,清理浏览器缓存。静态资源文件名带 hash,但 index.html 可能被浏览器缓存成旧版本,部署后先强制刷新一次,确认不是缓存问题再动代码。
6.2 后端 jar 包启动的端口冲突与静态资源 404
后端部署时最常用的命令是:
java -jar furniture-backend.jar --server.port=8080 --spring.config.location=/data/furniture/application-prod.yml--server.port可以覆盖 jar 内打包的端口,--spring.config.location指定外部配置文件,这样不同环境不用重新打包。启动如果报端口被占用,先用以下命令确认占用进程:
lsof -i :8080静态资源 404 的问题通常出自两个地方:上一个章节里讲的addResourceHandlers路径写死成 Windows 盘符,以及图片上传接口的保存目录没有创建。部署完可以找一个图片 URL,直接 curl 看返回状态码是 200 还是 404,能快速定位是映射问题还是目录问题。
6.3 把验证结果写成论文的系统测试数据
部署验证完,顺手把数据整理成论文“系统测试”章节的表格。测试用例表通常包含四列:用例编号、功能模块、操作步骤、预期结果。不要只写“功能正常”这种空话,填符合实际观测到的响应时间,例如“首页加载 8 件商品,接口响应在 120ms 以内”。这类数据是你自己在部署环境上真实验证出来的,写进毕业论文比引用任何二手资料都有说服力。
本文还有配套的精品资源,点击获取