ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

网络海鲜市场系统平台:SpringBoot+Vue+MySQL全栈项目实战解析

网络海鲜市场系统平台:SpringBoot+Vue+MySQL全栈项目实战解析 毕业设计做全栈项目最怕的不是功能写不出来而是写完以后讲不清楚为什么这么设计、别人问起来答不上来。这篇博文我想从我的真实经验出发完整复盘一个基于SpringBootVueMySQL的网络海鲜市场系统平台从选题、数据库设计、后端接口、前端页面到最终部署到云服务器跑通全流程把每一步的决策逻辑和踩过的坑都摊开来讲。这个项目涵盖了完整的用户端购物流程、商家端商品管理和平台端运营监控源码、数据库脚本、论文和部署文档都是齐全的不管是想直接参考做类似课题还是想理解一个全栈电商系统是怎么从零搭起来的都有实际价值。下面我会按真实开发顺序来拆解而不是按教科书结构来讲。1. 为什么选“网络海鲜市场”做毕设选题逻辑与技术栈权衡1.1 选题的动机和行业痛点电商系统是毕业设计里最常见的选题方向之一图书、服装、数码产品都已经被人做烂了。我当初选“海鲜市场”这个垂直领域核心原因是这个方向有足够多的业务特性可以去挖掘而不是停留在最基础的增删改查。海鲜产品和普通商品有几个非常明显的差异计量单位特殊海鲜类商品按份、按斤、按只计价的情况都有而且同一个商品可能有多种规格比如大闸蟹可以按“3两公蟹”“4两母蟹”“6只装礼盒”来卖。批次和新鲜度敏感海鲜讲究捕捞批次和到货日期同一款商品的库存是按批次管理的不能像卖手机那样只维护一个总库存数。物流和售后特殊海鲜运输需要冷链运输途中出现死亡、破损的赔付规则跟普通商品完全不一样。价格浮动大会受到捕捞量、季节、天气、节假日等因素影响商品价格不能像普通电商那样长期稳定。这些业务特点决定了系统的功能设计必须比普通电商多出几块批次管理、规格管理、按收货地址计算配送时间、售后原因里加入“运输死亡”“品质异常”等选项。这也是我在论文里能够写深的一个方向答辩的时候老师也会觉得这个选题有真实业务场景做支撑不是凭空造的。1.2 为什么是SpringBootVueMySQL而不是其他组合技术栈选型的时候我身边有不少同学选的SSM、JSP、PHP甚至还有用Python Django的。我最后定SpringBootVueMySQL理由很实际SpringBoot解决的是后端开发效率的问题。它把Spring繁杂的XML配置变成了自动配置内嵌了Tomcat一个java -jar就能跑起来。社区资料多到数不清遇问题搜一下基本就有答案。对于毕设这种把“按时保质完成”放在第一位的场景来说这是一个巨大的优势。Vue解决的是前端开发体验的问题。组件化开发让页面可以像搭积木一样拆开来写购物车、商品卡片、后台管理表格这些能复用的模块做成组件后面改起来很方便。响应式数据绑定让页面状态和展示自动保持同步不用像JQuery时代那样频繁操作DOM。MySQL则是高校和中小型项目的绝对主流。开源免费、稳定可靠、相关的教程和工具链极其成熟。海鲜市场这个规模的业务量MySQL的读写性能完全够用而且InnoDB引擎的事务支持刚好能满足电商下单流程对数据一致性的要求。另外还有一个很现实的原因这三个技术栈是当前国内中小型公司里使用率非常高的一套组合。毕设做完以后不管是简历上写项目经历还是面试时聊技术方案这套栈的认可度都更高面试官也更熟悉沟通成本低很多。1.3 交付物的完整构成这个项目最终交付的内容一共四块每一块都有自己的用途源码后端SpringBoot工程加前端Vue工程完整的可编译可运行代码。后端按controller、service、mapper分层前端按视图组件、路由、状态管理、API请求分层。数据库脚本包含建库建表语句、初始化数据脚本和核心存储过程。拿到脚本以后能直接恢复出一套带测试数据的库省掉手动造数据的时间。论文从选题背景、需求分析、概要设计、详细设计、系统实现到测试的完整文档大约两万字左右。论文的结构和代码结构是严格对应的方便边看代码边对照文档。部署文档从环境准备、配置文件修改、前后端打包到服务器部署的完整过程记录本地跑通和云服务器上线都覆盖到了。2. 系统业务梳理三类角色与六大核心模块2.1 角色权限划分这个系统的用户分三类平台管理员、入驻商家、普通消费者。很多人做电商系统会把商家端和管理员端合并成一个后台看起来省事但实际上业务逻辑混乱论文也不好评写。我把它们拆开了各自独立成端。C端用户游客可以浏览商品和搜索登录注册后可以加入购物车、下单购买、发表评价、申请售后、管理收货地址和个人资料。商家端商家用户登录后进入独立的后台界面可以管理自己的商品、上下架、维护库存批次、处理订单发货、回复用户评价。平台管理端管理员负责平台整体运营包括用户管理、商家入驻审核、全平台订单监控、分类管理、公告发布和数据统计。权限控制是在后端做的前端只是做展示层的隐藏。接口层面用SpringBoot拦截器加JWT令牌校验再通过角色字段做接口级权限区分这个后面会详细讲。2.2 六大核心模块的功能清单整个系统按业务域划分成六个大模块用户模块注册、登录、找回密码、个人信息维护、收货地址管理。注册时区分普通用户和商家角色商家注册后需要平台管理员审核才能进入商家后台。商品模块商品分类、商品列表、商品详情、商品搜索和筛选。筛选条件可以按分类、价格区间、产地、发货方式组合查询。商品详情页展示多图轮播、规格选择、库存余量、累计销量和用户评价。购物车模块加购、修改数量、删除、批量结算。购物车中的商品信息以快照方式保存避免商品价格变动后用户结算时产生疑惑。订单模块确认订单、提交订单、订单支付模拟、商家发货、用户确认收货。订单状态机是整个系统业务的核心待付款、待发货、待收货、已完成、已取消、售后中这几个状态之间的流转规则在数据库设计和后端代码里都有明确约束。评价与售后模块订单完成后用户可以评价评价内容包括评分、文字内容和图片。售后申请填写的类型包含“运输死亡”“品质异常”“数量不符”“其他原因”商家端可以对售后申请进行审核处理。数据统计模块管理员端有平台销售额趋势、订单量变化、热门商品排行商家端有自己店铺的销售统计和商品访问统计。统计页面用ECharts展示图表。2.3 容易被忽略的业务细节为什么这些设计很重要有几个细节是普通电商系统里看不到的但对海鲜市场来说恰恰是核心批次号字段。商家上架商品时除了填商品基本信息还需要填批次号、捕捞日期、产地、到货日期。这样当用户买到某个批次的商品后台可以根据批次定位问题源头。数据库里商品表和批次信息是两个字段关联而不是混在一起。按重量计价的支持。海鲜的价格通常有“斤价”和“份价”两种。我设计了一个规格表一个商品对应多个规格每个规格有独立的标题比如“3两母蟹 6只”、价格和库存。下单时选的不是商品而是商品的某个规格。这个设计让系统可以直接支持“大份、中份、小份”这类常见售卖方式。配送时效的差异化提示。非海鲜商品可以发普通快递海鲜商品需要冷链配送。商品表里有个字段标识是否冷链如果是冷链商品结算页会提示“冷链专送预计48小时内送达”这不是随便写的文案而是根据物流模板ID计算的时效展示。库存与下单的高一致性。海鲜库存数量必须准确不能超卖也不能负库存。后端在下单事务里作了库存扣减的并发控制后面在核心实现章节会展开。3. 数据库设计12张核心表是怎么设计的3.1 核心表清单数据库设计我花了整整两天时间反复调整了三版才定稿。数据库是整个系统的地基一旦表结构设计有问题后期代码写得再漂亮都白搭。最终设计是12张核心表覆盖所有业务。表名职责关键字段说明user用户信息用户名、密码BCrypt加密、手机号、角色、头像address收货地址用户ID、省市区、详细地址、默认标识category商品分类分类名称、上级ID、排序号product商品主表分类ID、商家ID、标题、描述、主图、详情图、是否冷链、状态product_sku商品规格商品ID、规格标题、价格、原价、库存、销量product_batch批次信息商品ID、批次号、产地、捕捞日期、到货日期、保质期cart_item购物车用户ID、SKU ID、数量、选中状态orders订单主表订单号、用户ID、商家ID、总金额、状态、地址快照、物流单号order_item订单明细订单ID、SKU快照、商品名快照、单价、数量、小计review评价订单ID、用户ID、商品ID、评分、内容、图片after_sale售后订单ID、用户ID、类型、原因、申请金额、处理状态notice公告标题、内容、发布时间、是否置顶3.2 商品表和订单表的关键字段设计product表中有一个字段很容易被忽略status。这个字段的值是0草稿、1上架中、2下架、3审核中。商家新增商品后平台没有做强制审核的情况下我简化成了商家可以直接上架但要保证status的管理逻辑清晰。一个商品如果被下架了用户在C端就看不到但在商家后台仍然可以查看和修改重新上架后恢复展示。orders表中我做了地址快照把收货人姓名、电话、完整地址直接存进订单表而不是通过address_id去关联。为什么这么做因为用户之后修改地址不会影响历史订单的发货信息。电商系统这块有个专业概念叫“数据快照”目的就是保证订单数据的不可变性。同样的道理order_item里也冗余存了商品名称、商品主图和规格标题做快照处理。虽然这违背了数据库第三范式但它是业务正确性优先的真实选择我在论文里也专门解释了这一点。订单状态字段status我用的是整数枚举取值如下CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 用户ID, merchant_id bigint(20) NOT NULL COMMENT 商家ID, total_amount decimal(10,2) NOT NULL COMMENT 总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态:0待付款,1待发货,2待收货,3已完成,4已取消,5售后中, receiver_name varchar(50) NOT NULL COMMENT 收货人姓名, receiver_phone varchar(20) NOT NULL COMMENT 收货人电话, receiver_address varchar(255) NOT NULL COMMENT 收货完整地址, remark varchar(255) DEFAULT NULL COMMENT 订单备注, pay_time datetime DEFAULT NULL, ship_time datetime DEFAULT NULL, finish_time datetime DEFAULT NULL, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_merchant_id (merchant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;3.3 库存扣减与订单状态流转库存扣减是电商系统里典型的高并发数据一致性问题毕设虽然不会面临真正的并发压力但代码里必须体现出“我懂这个问题”。我的方案是在order_item创建时才对SKU库存做扣减而不是在用户提交订单前就预扣。具体流程用户提交订单时后端进入一个Transactional事务方法先锁定对应的SKU行SELECT ... FOR UPDATE然后判断剩余库存是否充足充足则扣减并创建订单和订单明细不足则抛异常让事务回滚。为什么不用提交订单前预扣库存的方案因为如果用户下单后不付款预扣的库存一直被占用同一批海鲜会被“锁死”影响其他真实买家。所以我在设计上选择了下单即扣减同时配合一个简单的定时任务超过30分钟未付款的订单自动取消并回补库存。订单状态流转我专门画过一张状态机图论文里用了核心规则如下待付款-待发货用户点击支付模拟支付成功后进入待发货。待付款-已取消用户主动取消或超时未支付被系统自动取消。待发货-待收货商家发货填入物流单号。待收货-已完成用户确认收货或者系统在发货后第7天自动确认收货。已完成-售后中用户在确认收货后7天内发起售后申请。售后中-已完成商家处理售后结束订单回到已完成状态或单独标记退款金额。3.4 为什么海鲜商品要用批次和保质期管理前面提到product_batch表这块设计我参考了食品行业的溯源逻辑。普通电商系统的商品表只有库存总量食品类商品则还需要追溯来源。商品和批次的关系是一个商品可能有多条批次记录当前展示的库存是各批次库存之和。商家在录入商品时可以选择绑定已有批次或新建批次。用户下单时订单明细里会记录商品当时对应的批次号。一旦某个批次的商品出现质量投诉商家可以登录后台按批次号筛选售后记录快速定位是哪个产地、哪一天的货出了问题。另外批次的expiry_date字段还有一个隐藏作用商品列表页可以把临近过期的批次商品自动标记为“临期特惠”提高转化率。我在项目里实现了这个标记的查询逻辑但没有做成自动降价这种复杂功能只是作为详情页的一个展示标签。4. 后端核心实现从接口分层到电商关键逻辑4.1 项目分层与依赖选型后端工程我按照标准的SpringBoot三层架构组织controller负责接收请求和参数校验service负责业务逻辑mapper数据访问层使用MyBatis-Plus操作数据库。分层最大的好处是职责清晰Controller里不写业务规则Service里不出现SQL片段Mapper里只有数据库操作出了问题很容易定位。pom.xml里的核心依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency为什么选MyBatis-Plus而不是原生MyBatis因为MyBatis-Plus提供了通用Mapper和条件构造器单表查询基本不用写SQL开发速度很快。复杂查询比如订单列表带商品快照的多表关联可以用Select注解写在Mapper接口里灵活性和可控性都兼顾了。JWT使用jjwt这个库它比早期的java-jwt更干净API设计也更现代。需要注意的是JJWT的版本和JDK版本有兼容性要求如果用的是JDK8最好选用0.11.x版本而不是最新的1.x版本。这个是很多人容易踩的坑。4.2 登录认证与权限拦截登录流程用的是JWT无状态认证方案。用户提交用户名和密码后后端用BCrypt校验密码校验通过后用用户ID和角色生成一个有效期为24小时的Token返回给前端。前端把Token存到localStorage之后每个请求的Authorization请求头都带上这个Token。拦截器实现的核心代码如下Component public class JwtInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Claims claims JwtUtil.parseToken(token); Integer userId (Integer) claims.get(userId); // 与Redis中的Token比对支持服务端主动失效 String redisKey login:token: userId; if (token.equals(redisTemplate.opsForValue().get(redisKey))) { request.setAttribute(userId, userId); request.setAttribute(role, claims.get(role)); return true; } } catch (Exception e) { // Token解析失败或过期 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }角色权限控制是通过自定义注解RequireRole加在Controller方法上实现的拦截器校验完JWT后继续校验角色是否匹配。比如商家发货接口标注RequireRole(MERCHANT)管理员的数据统计接口标注RequireRole(ADMIN)代码简洁也可以直接在方法上安全边界清晰。这里有一个我在实际开发中调整过的方案Token为什么要同时存Redis如果只靠JWT自身的过期时间服务器无法主动让某个用户的Token失效比如用户修改密码后旧Token仍然有效这就不安全了。存一个Redis副本以后服务端在改密码、封号、退出登录时可以主动删除对应的Token记录。毕设答辩的时候讲出这一层设计明显比单纯的JWT方案更有说服力。4.3 下单与库存扣减一个容易翻车的并发场景下单接口是整个项目里业务逻辑最复杂的接口没有之一。它的核心流程是接收购物车选中的SKU列表和地址信息计算出总金额生成订单扣减库存清空购物车对应项返回订单号。这里最怕的是两个用户同时买最后一份海鲜结果两个人都下单成功了导致库存变成负数。解决这个问题我在sku表中先执行带锁的查询把数据行锁住再判断库存并扣减Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateRequest req) { // 1. 校验地址与购物车项 // 2. 生成订单号格式: 时间戳用户ID随机数 String orderNo generateOrderNo(); // 3. 遍历SKU列表逐行锁定 for (OrderItemRequest item : req.getItems()) { ProductSku sku productSkuMapper.selectByIdForUpdate(item.getSkuId()); if (sku null) { throw new BusinessException(商品规格不存在); } if (sku.getStock() item.getQuantity()) { throw new BusinessException(商品库存不足 sku.getTitle()); } // 4. 扣减库存 ProductSku update new ProductSku(); update.setId(sku.getId()); update.setStock(sku.getStock() - item.getQuantity()); update.setSales(sku.getSales() item.getQuantity()); productSkuMapper.updateById(update); } // 5. 插入订单与订单明细 // 6. 删除购物车中已购买的SKU return orderVO; }selectByIdForUpdate这个方法使用了MyBatis-Plus的自定义SQL写法在Mapper里自己写了一句SELECT * FROM product_sku WHERE id #{id} FOR UPDATE。同一时刻只有拿到行锁的事务才能更新这行SKU数据另一个事务会一直等到前一个事务提交后才继续天然避免了超卖。这里有个细节值得注意事务一定要放在Service层因为只有Spring管理的代理对象上的Transactional注解才会生效。如果放在Controller层事务是无效的。另外加锁范围只针对单品SKU的行锁而不是锁整个表这样不同SKU的并发下单可以并行执行性能不会差。4.4 统一异常处理与全局返回体接口返回格式我统一封装成ResultT结构是code message data。成功时code200业务异常code400未登录code401权限不足code403系统异常code500。前端拿到任何响应都可以用同一套逻辑处理不需要每个接口单独做判断。后端提供了一个GlobalExceptionHandler用RestControllerAdvice捕获异常。业务流程中抛出BusinessException会被统一转成code400并返回给前端展示具体原因比如“库存不足”“订单不存在”“不能对自己发布的商品下单”等等。而未被预料的系统异常会被记入日志返回一个比较模糊的提示给用户不暴露堆栈信息。一开始写代码的时候我曾经把异常处理逻辑写在每个Controller方法内部的try-catch里一个人写10个接口还好写到30个接口的时候代码里全是重复的catch块又乱又难维护。后来改成了全局异常处理Controller只关心正常流程异常全部交给切面处理代码量少了将近三分之一。这一点我觉得值得每一个做毕设的人借鉴。4.5 文件上传与图片访问海鲜商品要有吸引人的图片商品详情页要支持多图展示。文件上传功能实现不难但有个坑项目重新打包部署之后上传的图片文件会因为target目录被清空而丢失。我的解决方案是把上传文件存到服务器某个独立的目录比如/www/seafood/upload/通过一个WebMvcConfigurer配置类把本地磁盘路径映射成/upload/**的虚拟访问路径。Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); } }file.upload-dir在application.yml里配置本地开发指到本机的一个绝对路径服务器上指到/www/seafood/upload。前端上传后用返回的相对路径拼接图片地址即可比如http://localhost:8080/upload/xxx.jpg。部署到服务器以后只需要把域名前缀换掉图片地址的拼接逻辑不用改。5. 前端落地Vue3还是Vue2、路由权限与Axios封装5.1 技术选型的纠结与实际取舍前端我纠结了很久是选Vue2还是Vue3。Vue3的Composition API更现代、性能更好生态也已经很成熟了Vue2则更稳定、踩坑资料更多、和Element UI配合最成熟。最终我选了Vue3加Element Plus组合理由是Vue是当前学习的重点方向面试时新项目的技术栈用Vue3体现出对新技术的学习能力。Element Plus的组件库在Vue3下已经很稳定表格、表单、分页、上传这些毕设高频组件都有现成实现。script setup语法在Vue3.2之后非常简洁写起来比Vue2的Options API体验好不少。工程构建工具用的是Vite而不是Vue CLI。Vite冷启动速度秒开开发体验好打包也快。Vue CLI虽然稳定但基于Webpack每次启动都要等好几秒甚至十几秒确实不太想再体验了。不过需要注意Vite要求Node.js版本不能太低开发机上装Node 16.15以上比较稳妥。5.2 路由设计与登录守卫前端路由分为两部分C端用户端和后台管理端。C端的核心路由有首页、商品列表、商品详情、购物车、订单结算、个人中心包含我的订单、我的评价、我的售后、地址管理后台管理端路由有仪表盘、商品管理、订单管理、售后管理、数据统计、用户管理、公告管理等。路由守卫是前端权限控制的第一道关卡。未登录用户访问需要登录的页面时跳转到登录页并携带redirect参数登录成功后自动跳回原页面。已经登录的用户去访问登录页会被直接弹回首页。商家用户访问/admin下的后台页面时会在守卫中校验角色如果不是商家会提示“无权限访问”。路由守卫逻辑router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }) return } if (to.meta.adminOnly) { const role localStorage.getItem(role) if (role ! MERCHANT role ! ADMIN) { next(/) return } } next() })5.3 Axios封装与Token注入前端网络请求统一用Axios封装成一个模块核心处理三件事请求拦截器自动从localStorage读取Token设置到Authorization请求头。响应拦截器判断HTTP状态码和后端返回的code如果返回401就清除本地登录信息并跳转登录页。统一的错误提示把后端的message字段展示给用户前端就不用每个页面单独处理错误逻辑了。还有一个容易忽略的点文件上传接口需要单独设置Content-Type为multipart/form-dataAxios如果拦截器默认加了application/json就可能导致后端解析失败。我在封装时给上传专用请求单独走了一个uploadRequest实例避免污染普通请求实例。5.4 典型页面拆解商品列表、购物车与订单结算商品列表页核心功能是分类筛选和搜索。左侧是分类菜单顶部是搜索框中间是商品卡片列表。商品卡片组件接收一个product对象点击后跳转详情页。搜索逻辑是后端接口通过keyword参数做商品标题模糊匹配分类参数则做精确过滤。这个页面前端只是“展示者”真正的筛选逻辑全在后端好处是列表数据量大时前端性能不吃紧。购物车页面需要注意的交互是“选中结算”。每行有一个复选框底部实时计算所有选中商品的总价。这个状态我用了一个计算属性selectedItems通过遍历cartList判断每个项的checked字段。提交订单时请求体里带的是所有checkedtrue的SKU项。订单结算页是最需要细心设计的页面。上半部分是收货地址展示最上面是默认地址可以点击切换或新增中间是商品清单每一行显示商品缩略图、名称、规格标题、单价和数量下方显示配送说明冷链还是普通快递是否需要加运费最底部是总金额结算按钮。提交订单前有一个二次确认弹窗展示商品数量、总金额、预计送达时间用户确认后才真正下单。5.5 基于角色渲染菜单后台管理端的侧边栏菜单不是写死的而是根据登录用户的角色动态生成。管理员能看到“用户管理”“平台订单”“数据统计”商家看不到这些只能看到“商品管理”“店铺订单”“售后处理”。前端维护了一个菜单配置数组每个菜单项带roles字段渲染时用v-if判断当前角色是否命中。这个方案牺牲了一点动态性新增菜单需要改前端代码但对毕设完全够用而且比完全由后端返回菜单列表再动态渲染的方案简单很多也好调试。真正生产级的动态菜单用后端下发的方式但那是属于架构升级的内容了。5.6 组合式API和选项式API怎么选关于Vue3的两种写法我的建议是新写的业务代码用script setup组合式API逻辑清晰、复用性强特别是购物车这种多个状态互相依赖的场景用ref和computed组织起来比Vue2的data和computed更直观。script setup import { ref, computed } from vue import { getCartList, deleteCartItem, updateCartQuantity } from /api/cart const cartList ref([]) const loading ref(false) const selectedItems computed(() { return cartList.value.filter(item item.checked) }) const totalAmount computed(() { return selectedItems.value.reduce((sum, item) sum item.price * item.quantity, 0) }) async function loadCart() { loading.value true try { const res await getCartList() cartList.value res.data } finally { loading.value false } } function onQuantityChange(item) { updateCartQuantity(item.id, item.quantity) } loadCart() /script选项式API也不是不能用但Vue3官方现在主推组合式学新项目直接上手组合式后面做复杂组件时能少走很多弯路。最初写后端一样把组合式API当作标准的函数拆分解法状态、计算属性和业务方法都围绕“这个页面有什么数据、依赖什么计算、能做什么操作”来组织很容易阅读。6. 本地开发到云部署一套能跑通的完整清单6.1 本地环境配置本地跑这个项目需要提前装好以下环境JDK 1.8或以上推荐JDK 8和JDK 11都试过都能跑Maven 3.6以上MySQL 8.0本项目用的是8.0.28版本Node.js 16.15以上最好用18 LTSIDE后端用IDEA前端用VS Code或者IDEA配上Vue插件也行MySQL 8.0的安装是个老生常谈又容易踩坑的点。Windows下面装的话我建议直接下载官方安装包走安装向导流程安装时记得选Server only编码集选utf8mb4认证方式选择Use Strong Password Encryption (Recom)加MySQL 8的默认认证方式但后面用JDBC连接时要注意驱动版本。6.2 后端打包jar包生成后端在IDEA里先用Maven插件做一次clean package跳过测试用-Dmaven.test.skiptrue。打包完成后在target目录下会出现一个seafood-server.jar这个jar包内嵌了Tomcat可以直接启动。打包之前的配置有两点要注意application.yml里按环境区分配置。本地环境用application-dev.yml数据库地址是localhost:3306服务器环境用application-prod.yml数据库地址改成云数据库的内网IP或者公网IP。SpringBoot用spring.profiles.active决定加载哪个配置。数据库连接的时区参数一定要加serverTimezoneAsia/Shanghai否则MySQL 8.0会报时间相关的错误。启动命令java -jar seafood-server.jar --spring.profiles.activeprod如果日志文件有乱码问题启动命令里追加-Dfile.encodingUTF-8。6.3 前端构建npm run build产物前端工程在开发模式跑通后部署前需要执行一次打包npm install npm run buildVite会生成一个dist目录里面是纯静态文件。关键配置在前端环境变量文件.env.productionVITE_API_BASE_URL/api这样前端代码里所有接口请求都以/api开头的相对路径发出去由Nginx反向代理转发到后端服务。好处是前端资源和后端服务可以放在同一个域名下避免跨域问题也方便后续配置HTTPS。6.4 Nginx反向代理与项目上线服务器上我使用Nginx做静态文件服务和反向代理。核心配置片段如下server { listen 80; server_name your-domain.com; root /www/seafood/dist; index index.html; # 前端路由history模式所有非静态资源请求回退到index.html location / { try_files $uri $uri/ /index.html; } # 对接后端API location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 静态上传文件 location /upload/ { alias /www/seafood/upload/; } }这段配置解决了两个非常经典的问题。第一个是Vue的history路由模式直接刷新/product/123这个页面时Nginx会找不到对应的静态文件返回404。try_files配置把所有请求都重写到index.html由前端路由接管。第二个是接口转发前端请求/api/list会被转发成后端的/list同时把原始Host和客户端IP传给后端这样后端的日志里能看到真实来源IP。6.5 远程服务器上的MySQL 8.0安装不得不手工做的几件事很多人在服务器上安装MySQL 8.0会卡在几个莫名其妙的环节。我在部署文档里专门总结了这几个步骤初始化密码问题。Ubuntu或CentOS通过apt或yum安装mysql-server后初始密码是随机生成的会在日志文件里打印。可以用grep temporary password /var/log/mysqld.log查第一次登录后必须改密码。如果怎么都找不到初始密码可以通过跳过授权表的方式重置mysqld_safe --skip-grant-tables但要记得重置完成后去掉这个参数并重启服务。远程连接授权。默认情况下MySQL只允许本机连接需要在MySQL命令行执行-- 创建专用用户并授权 CREATE USER seafood% IDENTIFIED BY 你的密码; GRANT ALL PRIVILEGES ON seafood_db.* TO seafood%; FLUSH PRIVILEGES;同时要修改my.cnf里的bind-address为0.0.0.0或者直接注释掉这一行否则即使授权了也连不上。防火墙和安全组。华为云、阿里云这类云服务器除了在服务器上用firewall-cmd放行3306端口还必须到云控制台的安全组规则里放行。如果本地用Navicat连接服务器数据库连不上先检查这个很多时候根本不是MySQL的问题。7. 那些差点被“毕设答辩”问倒的坑7.1 MySQL 8.0连接失败密码加密规则和驱动版本本地跑SpringBoot连MySQL 8.0时最常见的问题是启动时报Public Key Retrieval is not allowed或Unable to load authentication plugin caching_sha2_password。根因是MySQL 8.0默认的密码加密方式是caching_sha2_password而早期版本的JDBC驱动不支持这种加密方式。解决办法有两个一是改用MySQL 8.0对应的JDBC驱动版本mysql-connector-java8.0.x二是在JDBC连接串末尾加上allowPublicKeyRetrievaltrueuseSSLfalse。我在本地开发时遇到过一种更隐蔽的情况Maven仓库里引入了MySQL 8.0的驱动但pom.xml里没写runtime作用域导致SpringBoot启动时加载了多个驱动版本产生了奇怪的连接异常。删掉多余版本、只保留一个8.0.x驱动后就好了。7.2 后端跨域问题CORS配置为什么有时候不生效前后端分离项目开发时前端通过localhost:5173访问后端localhost:8080必然触发跨域。我的解决方式是在后端写了一个全局CORS配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有个容易出错的点allowCredentials(true)时allowedOrigins不能写成*必须用allowedOriginPatterns(*)否则部分浏览器会拒绝带有凭证的跨域请求。另外如果后端同时配置了拦截器注意OPTIONS预检请求必须放行否则前端发出的预检请求会被拦截器拦截同样导致跨域失败。不过实际上线部署时我推荐用Nginx转发的方式从根本上规避CORS问题。前端和后端在同一个域名和端口下自然不存在跨域。7.3 上传图片无法访问本地路径与虚拟映射图片上传成功但访问404这是文件上传功能最常见的坑。开发时我用IDEA的file.upload-dir指到D:/seafood/upload访问http://localhost:8080/upload/xxx.jpg正常部署服务器后我把路径改成/www/seafood/uploadNginx配置里也配了alias但仍然出现404。排查过程先在服务器上用curl http://127.0.0.1:8080/upload/xxx.jpg直接测试后端接口发现能返回图片再用curl http://127.0.0.1/api/upload/xxx.jpg测试Nginx转发路径发现404。原因是我的Nginx后端转发配置把/api/前缀剥掉了proxy_pass http://127.0.0.1:8080/;后面的斜杠会导致URL重写但图片请求路径不是通过/api走的所以根本没被转发。最终把/upload/的静态资源路径单独配在Nginx层指向服务器磁盘目录问题解决。7.4 商品超卖扣库存前必须做的校验和行锁超卖问题前面已经提过但这里想补充一个我在自测时发现的边界条件如果用户在下单时恰好另一个请求把商品下架了库存扣减依然会成功因为库存表和商品表是两个独立操作没有做联合状态校验。后来在扣减库存前加了一步判断该商品所属product.status必须是上架中1否则直接拒绝下单。这个校验要和库存判断放在同一个事务中才能保证业务一致性。还有一个小坑是MyBatis-Plus的updateById方法默认只更新非NULL字段。如果你希望把库存直接改成stock - quantity而不是传入计算后的具体值要小心并发。我在代码里是先selectByIdForUpdate拿到最新库存再计算新值更新过程中行锁保证了不会有其他事务插入中间状态。如果不加行锁直接UPDATE product_sku SET stock stock - #{quantity}其实也能保证原子性但前提是数据库层面的SQL和MyBatis-Plus的UpdateWrapper写法要正确。两种方式我都实现过最后选了先查询再更新这样代码逻辑清晰也方便加各种业务校验。7.5 Vue打包后刷新404history路由与Nginx配置部署完成后用户从首页点进商品详情页再刷新页面直接404。这个问题本质上是Vue的createWebHistory路由模式与Nginx静态资源服务的冲突。用户访问/product/123时服务器上没有这个真实文件Nginx默认返回404。解决方案就是Nginx配置里的try_files $uri $uri/ /index.html;这一行。它会把所有匹配不到真实资源的请求统一交给index.html由Vue Router根据URL重新渲染对应组件。但这里有个注意点history模式会让所有404请求都返回index.html然后由前端做兜底。如果后端接口也没有对应的路径前端路由不会匹配到组件时就会显示空白页或者404页面。我为此专门在前端路由表末尾加了一个catch-all路由匹配所有未定义路径并跳转到404页面组件这样用户看到了“页面不存在”而不是白屏。7.6 答辩前最值得检查的三件事基于我自己的教训论文和答辩前有几件事值得认真检查数据库初始化脚本能否从一个干净的库完整跑通。我把seafood_db.sql删掉重建过很多次确保任何拿到项目的人能一键建库。测试账号和测试数据要齐。管理员账号、商家账号、普通用户账号各准备一个每种角色至少有一条正常的业务数据链路商品-购物车-订单-发货-评价。有测试数据演示的时候比临时造数据从容得多。配置文件不能泄露敏感信息。代码上传到Git或者发给老师时数据库密码和密钥不要写在配置里明文暴露。这时候可以用application-prod.yml放生产配置但不提交本地用application-dev.yml。也可以用Jasypt对配置文件中的密码做加密答辩时提一句“生产环境的敏感信息做了加密处理”属于加分项。写在最后的一点个人体会从选题到部署这个SpringBootVueMySQL的网络海鲜市场系统平台前前后后花了将近两个月。如果只说一句体会那就是毕设项目真正值钱的部分不是“代码能跑”而是“每一个设计都能说出为什么”。为什么订单表要存地址快照、为什么扣库存要用行锁、为什么前端菜单要按角色动态渲染这些问题想清楚了、答出来了整个人的技术水平会被系统性地拔高一个层次远远超过“抄一个项目改个名”的收获。最后再分享一个实用的小技巧SpringBoot启动时用Banner生成器生成一个个性化的启动横幅虽然不影响功能但演示给老师看的时候第一印象的加分效果是实实在在的。
返回列表