最近在帮几个学弟学妹看毕业设计,发现一个挺有意思的现象:很多人一上来就问我“有没有现成的项目代码”,拿到手之后,第一件事不是去理解业务逻辑和代码结构,而是直接找“登录注册”和“增删改查”的代码,然后开始机械地复制粘贴,试图拼凑出一个能跑起来的系统。
这种做法的结果往往是,项目虽然勉强能运行,但答辩时老师随便问几个深入点的问题,比如“为什么这里要用Redis缓存而不是本地缓存?”、“前后端数据交互的安全性问题你是怎么考虑的?”,或者“这个物流状态流转的逻辑,如果并发高了会有什么问题?”,就立刻卡壳了。
这让我意识到,很多同学缺的不是一个“项目”,而是一个能让自己真正理解“如何从零开始构建一个可运行、可讲解、有亮点的完整系统”的路径。今天,我们就以“家居物流管理系统”这个典型的毕业设计选题为例,抛开那些华而不实的“秒杀”、“高并发”噱头,回归到毕业设计的本质:如何通过一个具体的业务场景,串联起SpringBoot和Vue的核心技术栈,并在这个过程中,建立起解决真实工程问题的思维框架。
这个系统的价值,不在于它用了多少炫酷的技术,而在于它完整地呈现了一个业务系统从需求分析、技术选型、数据库设计、接口开发到前端联调的全链路思考过程。你会发现,把每个环节的“为什么”想清楚,远比堆砌一堆用不上的高级功能更重要。
1. 先想清楚:毕业设计的核心是“可讲解性”,而不是“复杂度”
很多同学在选题时容易陷入一个误区:觉得技术越新、功能越复杂,分数就越高。于是拼命往项目里塞Redis、MQ、Elasticsearch,结果自己都讲不清楚为什么要用,反而成了减分项。
对于“家居物流管理系统”这类偏业务管理的毕业设计,评委老师最看重的往往是以下几点:
- 业务逻辑是否清晰:能否用一两句话说清楚系统是干什么的?核心业务流程(如:客户下单 -> 仓库拣货 -> 物流派送 -> 客户签收)是否完整闭环?
- 技术选型是否合理:为什么用SpringBoot+Vue?它们在这个项目里各自解决了什么问题?有没有更简单的方案?(比如,如果只是静态展示,用纯SpringBoot模板渲染是不是更快?)
- 代码结构是否规范:Controller、Service、Dao分层是否清晰?有没有遵循一些基本的开发规范(如统一的响应封装、异常处理)?
- 是否有自己的思考:是照搬了某个教程,还是在某个环节(比如物流状态更新策略、权限设计)有自己的设计和实现?
所以,我们的首要目标不是做一个“大而全”的电商平台,而是做一个逻辑自洽、代码整洁、便于讲解的“教学样本”。基于这个目标,我们来拆解系统的核心模块。
1.1 定义最小可行产品(MVP)范围
一个家居物流系统,最核心的实体和流程是什么?我们可以先画出一个极简的领域模型:
- 用户:分为
客户(下单、查物流)、管理员(管理订单、商品、物流)。 - 家居商品:有分类、名称、库存、价格等属性。
- 订单:关联用户和商品,有状态(如
待付款、待发货、运输中、已签收、已取消)。 - 物流信息:关联订单,记录运输过程中的关键节点(如
已揽收、到达xx转运中心、派送中)。
基于这个模型,我们的MVP功能可以限定为:
- 用户注册/登录(区分客户和管理员角色)。
- 客户:浏览商品、加入购物车、下单、查看订单物流轨迹。
- 管理员:管理商品信息、审核订单、更新物流状态。
为什么先做MVP?因为毕业设计时间有限,先确保核心链路跑通。有了这个基础,你答辩时才能清晰地描述“我的系统完成了从下单到签收的完整流程”。如果时间充裕,再考虑扩展如库存预警、数据统计、短信通知等“加分项”,但前提是核心流程必须稳固。
1.2 技术栈选型背后的“为什么”
选SpringBoot和Vue,不能只说“因为它们火”。要理解它们在项目中的具体作用:
- SpringBoot:它最大的价值是“约定大于配置”,能让我们快速搭建一个可独立运行、内嵌Tomcat的后端服务。对于毕业设计,你不需要花大量时间去折腾XML配置和服务器部署,可以把精力集中在业务逻辑开发上。
- 关键依赖:除了
spring-boot-starter-web,我们至少还需要spring-boot-starter-data-jpa(或MyBatis-Plus)操作数据库,spring-boot-starter-security或sa-token处理权限,spring-boot-starter-validation做参数校验。
- 关键依赖:除了
- Vue 2.x:对于大多数学校的毕业要求,Vue 2的生态和资料足够丰富,学习曲线平缓。它的核心是组件化和响应式数据绑定,能让我们以模块化的方式构建前端页面,并且当后端数据变化时,前端视图能自动更新(比如物流状态变化)。
- 关键依赖:
vue-router(管理页面路由)、axios(发送HTTP请求)、element-ui或ant-design-vue(UI组件库,能极大提升开发效率)。
- 关键依赖:
一个常见的坑:很多教程会教你把前后端完全物理分离(两个独立工程),这虽然标准,但对于新手,在本地联调时可能会遇到跨域(CORS)问题。一个更稳妥的起步方式是:在SpringBoot中配置CORS,或者初期甚至可以将Vue项目打包后的静态文件直接放到SpringBoot的src/main/resources/static目录下,先以单体形式运行,确保功能无误后再考虑完全分离部署。这能帮你绕过第一个大坑。
2. 后端实战:从数据库设计到安全可用的API
后端是系统的“大脑”。很多同学在这里容易写出“能跑就行”的代码,但忽略了可维护性和安全性。我们按步骤来。
2.1 数据库设计:关系与状态是核心
家居物流业务中,表之间的关系和状态字段的设计是关键。
-- 以MySQL为例,核心表结构简析 CREATE TABLE `user` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `username` varchar(50) UNIQUE NOT NULL COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '加密后的密码', `role` varchar(20) NOT NULL DEFAULT 'CUSTOMER' COMMENT '角色:ADMIN, CUSTOMER', `phone` varchar(20) COMMENT '手机号', `create_time` datetime DEFAULT CURRENT_TIMESTAMP ) COMMENT '用户表'; CREATE TABLE `product` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `category_id` bigint COMMENT '分类ID', `name` varchar(100) NOT NULL, `price` decimal(10,2) NOT NULL, `stock` int NOT NULL DEFAULT 0 COMMENT '库存', `description` text, `image_url` varchar(500) ) COMMENT '商品表'; CREATE TABLE `order` ( `id` varchar(32) PRIMARY KEY COMMENT '订单号(可使用雪花算法生成)', `user_id` bigint NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` varchar(30) NOT NULL DEFAULT 'PENDING_PAYMENT' COMMENT '订单状态', `address` text NOT NULL COMMENT '收货地址', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ) COMMENT '订单表'; CREATE TABLE `order_item` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `order_id` varchar(32) NOT NULL, `product_id` bigint NOT NULL, `quantity` int NOT NULL COMMENT '购买数量', `price` decimal(10,2) NOT NULL COMMENT '下单时单价', FOREIGN KEY (`order_id`) REFERENCES `order`(`id`), FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ) COMMENT '订单明细表'; CREATE TABLE `logistics` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `order_id` varchar(32) NOT NULL UNIQUE COMMENT '一个订单一条主物流记录', `current_status` varchar(50) NOT NULL COMMENT '当前状态', `carrier` varchar(100) COMMENT '承运商', `tracking_number` varchar(100) COMMENT '运单号', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ) COMMENT '物流主表'; CREATE TABLE `logistics_detail` ( `id` bigint PRIMARY KEY AUTO_INCREMENT, `logistics_id` bigint NOT NULL, `status` varchar(100) NOT NULL COMMENT '节点状态', `location` varchar(200) COMMENT '地点', `description` varchar(500) COMMENT '描述', `event_time` datetime NOT NULL COMMENT '事件时间', FOREIGN KEY (`logistics_id`) REFERENCES `logistics`(`id`) ) COMMENT '物流轨迹明细表';设计要点解析:
- 订单号:不用自增ID,而用字符串(如雪花算法ID),避免暴露业务量,也便于线下沟通。
- 订单与物流:这里设计成
1对1的主物流记录和1对N的轨迹明细。为什么?因为一个家居订单可能是一个大件商品,一次运输就是一个整体。这种设计便于清晰展示物流全过程。 - 状态字段:
order.status和logistics.current_status是业务流转的核心。要明确定义每个状态的含义和转换规则(如待付款->待发货->运输中->已签收)。在代码中,建议使用枚举类来管理,避免魔法字符串。 - 数据冗余:
order_item.price存储了下单时的快照价格,这是必要的。因为商品product.price后续可能会变动,但订单金额不应随之改变。
2.2 构建清晰的三层架构与业务逻辑
避免把所有代码都堆在Controller里。标准的分层能让你的代码更易读、易测试。
com.example.logistics ├── controller // 接收请求,返回响应。职责要薄,只做参数校验和格式转换。 ├── service // 业务逻辑核心。事务管理、业务规则都在这里。 ├── repository // 数据访问层(JPA)或 mapper(MyBatis)。 ├── entity // 对应数据库表的实体类。 ├── dto // 数据传输对象,用于前后端交互,常和entity不同。 ├── vo // 视图对象,用于封装返回给前端的数据。 └── config // 配置类(如安全配置、CORS配置)。以“客户下单”这个核心业务为例,看看各层如何协作:
Controller层 (
OrderController): 只负责接收前端传来的商品ID列表、收货地址,验证用户是否登录,然后调用Service。@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public ResultVO<String> createOrder(@RequestBody CreateOrderDTO dto, HttpServletRequest request) { // 1. 从请求中获取当前用户ID(通常通过拦截器从Token中解析后存入) Long userId = (Long) request.getAttribute("userId"); // 2. 参数校验(可使用@Valid注解) if (dto.getProductItems() == null || dto.getProductItems().isEmpty()) { return ResultVO.error("商品列表不能为空"); } // 3. 调用业务层 String orderId = orderService.createOrder(userId, dto); return ResultVO.success("订单创建成功", orderId); } }Service层 (
OrderServiceImpl): 这里是重头戏,包含完整的业务逻辑和事务管理。@Service @Transactional // 关键!保证整个方法在同一个事务中 public class OrderServiceImpl implements OrderService { @Autowired private ProductRepository productRepo; @Autowired private OrderRepository orderRepo; // ... 其他Repository @Override public String createOrder(Long userId, CreateOrderDTO dto) { // 1. 验证商品是否存在、库存是否充足 List<OrderItem> items = new ArrayList<>(); BigDecimal totalAmount = BigDecimal.ZERO; for (CreateOrderDTO.Item itemDTO : dto.getProductItems()) { Product product = productRepo.findById(itemDTO.getProductId()) .orElseThrow(() -> new BusinessException("商品不存在")); if (product.getStock() < itemDTO.getQuantity()) { throw new BusinessException("商品[" + product.getName() + "]库存不足"); } // 2. 扣减库存(悲观锁或乐观锁,这里简单演示) int updatedRows = productRepo.deductStock(product.getId(), itemDTO.getQuantity()); if (updatedRows == 0) { throw new BusinessException("商品[" + product.getName() + "]库存扣减失败,可能已被其他用户购买"); } // 3. 构建订单项 OrderItem orderItem = new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setQuantity(itemDTO.getQuantity()); orderItem.setPrice(product.getPrice()); // 记录下单时价格 items.add(orderItem); totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity()))); } // 4. 生成订单号(雪花算法) String orderId = IdUtil.getSnowflakeNextIdStr(); // 5. 保存订单主表和明细 Order order = new Order(); order.setId(orderId); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(OrderStatusEnum.PENDING_PAYMENT.getCode()); order.setAddress(dto.getAddress()); orderRepo.save(order); // 设置关联并保存明细 items.forEach(item -> item.setOrderId(orderId)); orderItemRepo.saveAll(items); // 6. 初始化物流记录(可选,也可在支付后初始化) Logistics logistics = new Logistics(); logistics.setOrderId(orderId); logistics.setCurrentStatus(LogisticsStatusEnum.CREATED.getCode()); logisticsRepo.save(logistics); return orderId; } }这段代码的工程化思考:
- 事务 (
@Transactional):下单涉及库存扣减和订单创建,必须在一个事务里,要么全成功,要么全失败。 - 库存扣减:直接
set stock = stock - ?可能会超卖。上面用了自定义的deductStock方法(内部使用UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?),利用数据库的原子性实现悲观锁,更稳妥。这是面试常考点。 - 异常处理:使用了自定义的
BusinessException,建议配合全局异常处理器(@ControllerAdvice),将异常转化为友好的错误信息返回给前端,而不是暴露堆栈信息。
- 事务 (
Repository层:使用Spring Data JPA,方法名遵循规范就能自动生成查询,非常便捷。
@Repository public interface ProductRepository extends JpaRepository<Product, Long> { // 扣减库存 @Modifying @Query("UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity") int deductStock(@Param("id") Long id, @Param("quantity") Integer quantity); }
2.3 安全与接口规范:容易被忽略的毕业设计加分项
很多毕业设计项目忽略了安全性,这是展示你工程思维的好机会。
认证与授权:不要用明文存密码!使用
BCryptPasswordEncoder加密。使用JWT或Sa-Token实现无状态登录。在拦截器中验证Token,并将用户信息存入请求上下文。// 简化的登录逻辑 @Service public class AuthService { public String login(String username, String password) { User user = userRepo.findByUsername(username); if (user == null || !passwordEncoder.matches(password, user.getPassword())) { throw new BusinessException("用户名或密码错误"); } // 生成JWT Token,包含用户ID和角色 return JwtUtil.generateToken(user.getId(), user.getRole()); } }在Controller方法上使用
@PreAuthorize("hasRole('ADMIN')")或类似注解来实现方法级权限控制。统一的API响应格式:让前端处理更简单。
@Data public class ResultVO<T> { private Integer code; // 200成功,500失败,401未认证... private String message; private T data; public static <T> ResultVO<T> success(T data) { ResultVO<T> vo = new ResultVO<>(); vo.setCode(200); vo.setMessage("success"); vo.setData(data); return vo; } // ... 其他静态工厂方法 }参数校验:在DTO字段上使用
@NotBlank、@Min、@Pattern等注解,并在Controller参数前加@Valid,可避免大量if判断。
3. 前端实战:用Vue构建一个清晰的管理界面
前端的目标是提供一个直观的操作界面。对于毕业设计,不必追求极致的交互体验,但组件结构清晰、数据流明确是关键。
3.1 项目结构与核心配置
使用Vue CLI创建项目后,一个清晰的结构有助于管理。
src ├── api // 所有axios请求封装,按模块划分 ├── assets // 静态资源 ├── components // 公共组件(如PageHeader,物流状态标签) ├── router // Vue Router配置 ├── store // Vuex状态管理(如果状态简单,可先用Pinia或不用) ├── utils // 工具函数(如request.js封装axios,auth.js处理token) ├── views // 页面组件 │ ├── customer // 客户页面:Home.vue, ProductList.vue, Order.vue │ └── admin // 管理页面:ProductManage.vue, OrderManage.vue, LogisticsUpdate.vue └── App.vue关键配置点:
axios封装:在utils/request.js中统一设置baseURL、超时时间、请求/响应拦截器。在请求拦截器中为每个请求自动添加Authorization: Bearer ${token}头;在响应拦截器中统一处理401(未登录)跳转到登录页。- 路由守卫:在
router/index.js中,利用beforeEach钩子判断目标页面是否需要登录/管理员权限,并检查本地Token是否有效。 - 环境变量:创建
.env.development和.env.production文件,分别配置开发和生产环境的API基础地址。
3.2 典型页面开发:以“物流状态管理”为例
这个页面能很好地体现前后端数据交互和状态管理。
API层 (
api/logistics.js):封装所有物流相关的请求。import request from '@/utils/request' export function getLogisticsByOrderId(orderId) { return request({ url: `/api/logistics/order/${orderId}`, method: 'get' }) } export function updateLogisticsStatus(logisticsId, status, location, description) { return request({ url: `/api/logistics/${logisticsId}/status`, method: 'put', data: { status, location, description } }) }页面组件 (
views/admin/LogisticsUpdate.vue):<template> <div class="logistics-update"> <el-card> <div slot="header"> <span>物流状态更新</span> </div> <!-- 搜索区域 --> <el-form :inline="true" :model="searchForm"> <el-form-item label="订单号"> <el-input v-model="searchForm.orderId" placeholder="请输入订单号" clearable></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> </el-form-item> </el-form> <!-- 物流信息展示 --> <el-descriptions v-if="currentLogistics" title="当前物流信息" border> <el-descriptions-item label="订单号">{{ currentLogistics.orderId }}</el-descriptions-item> <el-descriptions-item label="当前状态"> <el-tag :type="getStatusTagType(currentLogistics.currentStatus)"> {{ currentLogistics.currentStatus }} </el-tag> </el-descriptions-item> <el-descriptions-item label="承运商">{{ currentLogistics.carrier }}</el-descriptions-item> </el-descriptions> <!-- 历史轨迹 --> <el-timeline v-if="logisticsDetails && logisticsDetails.length > 0" style="margin-top: 20px;"> <el-timeline-item v-for="(detail, index) in logisticsDetails" :key="index" :timestamp="formatTime(detail.eventTime)" placement="top"> <el-card> <h4>{{ detail.status }}</h4> <p>{{ detail.location }}</p> <p>{{ detail.description }}</p> </el-card> </el-timeline-item> </el-timeline> <!-- 状态更新表单 --> <el-divider></el-divider> <el-form :model="updateForm" label-width="100px" v-if="currentLogistics"> <el-form-item label="更新状态" required> <el-select v-model="updateForm.status" placeholder="请选择"> <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </el-form-item> <el-form-item label="地点"> <el-input v-model="updateForm.location"></el-input> </el-form-item> <el-form-item label="描述"> <el-input type="textarea" v-model="updateForm.description"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submitUpdate">提交更新</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script> import { getLogisticsByOrderId, updateLogisticsStatus } from '@/api/logistics' export default { name: 'LogisticsUpdate', data() { return { searchForm: { orderId: '' }, currentLogistics: null, logisticsDetails: [], updateForm: { status: '', location: '', description: '' }, statusOptions: [ // 与后端枚举对应 { label: '已揽收', value: 'COLLECTED' }, { label: '运输中', value: 'IN_TRANSIT' }, { label: '到达转运中心', value: 'AT_HUB' }, { label: '派送中', value: 'OUT_FOR_DELIVERY' }, { label: '已签收', value: 'DELIVERED' } ] } }, methods: { async handleSearch() { if (!this.searchForm.orderId.trim()) { this.$message.warning('请输入订单号') return } try { const res = await getLogisticsByOrderId(this.searchForm.orderId) this.currentLogistics = res.data.logistics this.logisticsDetails = res.data.details || [] this.$message.success('查询成功') } catch (error) { this.$message.error(error.message || '查询失败') } }, async submitUpdate() { if (!this.updateForm.status) { this.$message.warning('请选择状态') return } try { await updateLogisticsStatus( this.currentLogistics.id, this.updateForm.status, this.updateForm.location, this.updateForm.description ) this.$message.success('更新成功') // 更新后重新查询,刷新页面数据 this.handleSearch() // 清空表单 this.updateForm = { status: '', location: '', description: '' } } catch (error) { this.$message.error(error.message || '更新失败') } }, getStatusTagType(status) { const map = { 'DELIVERED': 'success', 'IN_TRANSIT': 'primary', 'CREATED': 'info' } return map[status] || 'info' }, formatTime(timeStr) { // 使用dayjs或moment格式化时间 return dayjs(timeStr).format('YYYY-MM-DD HH:mm:ss') } } } </script>这个组件演示了几个关键点:
- 数据驱动视图:
currentLogistics和logisticsDetails的变化会自动更新页面显示。 - 组件化:使用了Element UI的
el-card、el-timeline等组件,快速构建界面。 - 前后端分离的交互模式:通过
axios发起异步请求,根据响应结果更新数据和提示用户。 - 用户体验:提交成功后重新查询数据并清空表单,是一个完整的操作闭环。
- 数据驱动视图:
3.3 状态管理与路由权限
对于这个规模的项目,如果状态不复杂(主要是用户登录信息),可以不用Vuex/Pinia,直接用本地存储localStorage配合Vue的响应式数据。但如果涉及跨多个组件的复杂状态(如全局购物车),状态管理库会更有帮助。
路由权限可以在router.beforeEach中实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const userRole = localStorage.getItem('role') // 存储用户角色 // 检查目标路由是否需要认证 if (to.meta.requiresAuth && !token) { next('/login') return } // 检查目标路由是否需要特定角色 if (to.meta.roles && to.meta.roles.length > 0) { if (!userRole || !to.meta.roles.includes(userRole)) { next('/403') // 跳转到无权限页面 return } } next() })在路由配置中为管理页面添加元信息:
{ path: '/admin/logistics', component: () => import('@/views/admin/LogisticsUpdate.vue'), meta: { requiresAuth: true, roles: ['ADMIN'] } }4. 联调、部署与答辩准备:从“能跑”到“能讲”
代码写完只是第一步,让项目顺利运行并清晰展示,才是毕业设计成功的关键。
4.1 前后端联调与问题排查
这是最容易卡住新手的环节。遵循以下步骤可以少走弯路:
- 独立测试:先用Postman或Swagger(集成
springfox-swagger2)把后端所有接口调通。确保每个接口的输入、输出、错误码都符合预期。 - 解决跨域(CORS):如果前后端分开启动(如Vue运行在
localhost:8080,SpringBoot在localhost:8081),浏览器会因同源策略阻止请求。在SpringBoot中配置一个全局CORS过滤器:@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); // 生产环境应指定具体前端地址 config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } } - 前端代理(开发环境):在Vue项目的
vue.config.js中配置代理,将/api开头的请求转发到后端,避免跨域。module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', // 后端地址 changeOrigin: true } } } } - 联调顺序:先调通登录接口,拿到Token并存储。然后调一个简单的GET接口(如获取商品列表),再调复杂的POST/PUT接口(如下单、更新状态)。一定要看浏览器开发者工具(F12)的Network和Console标签,这里会显示请求是否成功、参数是否正确、响应是什么。
4.2 部署:最简单的可演示方案
答辩时,你需要一个正在运行的系统给老师看。最简单的部署方式是:
- 后端:使用
mvn clean package打包SpringBoot项目,生成一个可执行的jar文件。在服务器(或你自己的高性能笔记本)上安装Java运行环境(JRE),然后通过java -jar your-project.jar启动。确保服务器的防火墙开放了对应的端口(如8081)。 - 前端:在Vue项目下运行
npm run build,生成dist文件夹。将这个文件夹里的所有文件,复制到SpringBoot项目的src/main/resources/static目录下(注意:先清空static目录)。然后重新打包SpringBoot的jar。这样,访问http://服务器IP:8081就能直接看到前端页面,所有前端资源都由SpringBoot服务提供,彻底没有跨域问题。这是毕业设计演示最稳妥、最省事的方法。
4.3 答辩准备:如何讲出亮点和深度
代码跑起来只是及格,讲得好才能拿高分。准备答辩时,不要平铺直叙地介绍功能,要按以下逻辑组织你的陈述:
- 开场(1分钟):清晰介绍项目背景(家居物流管理的痛点)、你的系统目标(实现一个从下单到配送跟踪的全流程管理系统)和技术选型(SpringBoot + Vue,为什么选它们)。
- 核心业务流程演示(3-5分钟):以一个“客户下单-管理员发货-客户查看物流”的完整流程为主线,现场操作演示。这是最直观的部分。
- 技术难点与解决方案(重点!3-5分钟):这是展示你思考深度的地方。可以讲1-2个点,例如:
- 难点一:如何防止超卖?讲解你在下单Service中使用的“数据库乐观锁/悲观锁”方案,对比简单的先查询后更新,说明为什么你的方案更安全。
- 难点二:如何设计可追溯的物流状态?讲解你设计的
logistics和logistics_detail表结构,说明这样设计(1对N)比只用一个状态字段记录当前状态的优势(可以查看完整历史轨迹)。 - 难点三:如何保证API安全?讲解你使用的JWT认证、权限控制(
@PreAuthorize)、密码加密、参数校验等。
- 项目总结与展望(1分钟):总结你通过这个项目掌握了什么(全栈开发流程、数据库设计、事务控制、前后端交互)。可以简要提一下如果继续迭代,会加入哪些功能(如Redis缓存热门商品、定时任务检查超时未支付订单、生成数据报表等),体现你的规划能力。
最重要的建议:在你的代码关键处(如事务注解、锁更新、JWT验证拦截器)加上清晰的注释。在答辩时,可以主动打开这些代码文件给老师看,边指边讲,这比空谈概念有说服力得多。
通过这样一个从思考、设计、编码到讲解的完整过程,你完成的不仅仅是一个毕业设计项目,更是一套应对未来工作中业务系统开发的初级方法论。这才是毕业设计真正应该带给你的东西。