ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue农产品预售平台开发实战:从数据库设计到系统部署

SpringBoot+Vue农产品预售平台开发实战:从数据库设计到系统部署 接到一个“基于SpringBootVue的农产品预售平台管理系统”这样的项目第一反应是典型的Java全栈方向SpringBootMySQLMyBatis打包了后端主流技术栈Vue撑起前端界面再挂上“完整源码”明摆着就是给学生或者刚转行的朋友准备的实战项目。很多人在做类似毕设或简历项目时最大的问题不是写不出来而是不知道怎么把业务讲清楚、把技术点挖出深度。今天正好借这个题目把整个系统的设计与实现从头到尾拆开讲一遍前后端怎么配合、数据库怎么设计、哪些坑必须避开全部拿出来聊。先说这个系统是做什么的。农产品预售平台核心是围绕农产品展开的在线预订交易场景。种植户或农场提前发布预售信息消费者看到后下单支付定金或全款等农产品成熟后再统一发货。和普通电商不一样农产品有生长周期、季节性和库存不确定性所以系统必须解决两个核心问题一是“预售信息怎么高效发布和展示”二是“库存和订单状态怎么在多角色协作中保持一致”。整套系统包含用户端、商家后台和管理后台三条线涉及注册登录、商品浏览、下单支付、订单管理、库存扣减、数据统计等完整闭环。适合三类人看准备毕业设计的计算机专业学生、想快速搭建前后端分离项目练手的开发者、以及需要一套可扩展方案做农产品电商业务的创业者。如果你之前写过传统单体JSP项目再看这套SpringBootVue的方案最直观的感受就是前后端彻底解耦开发和调试都舒服很多。但真正做到既能跑起来、又能在讲项目时说清楚“为什么这么设计”还需要从整体架构到细节实现都过一遍。1. 项目整体设计与业务模块拆解农产品预售平台听起来简单实际上业务链路比一般电商系统要长。普通电商是“现货下单—立即发货”预售系统则是“下单—锁定预库存—生产/入库—发货结算”中间可能还穿插定金和尾款两个支付节点。所以设计系统之前先把业务流程理清楚是最关键的一步。1.1 用户角色与业务流程分析整个系统涉及三种角色权限划分是系统设计的骨架。消费者前台用户注册登录后可以浏览预售商品、查看详情、下单支付、查看订单状态、申请售后农场主/商家后台用户维护预售商品、设置预库存和生产周期、处理订单、更新发货状态系统管理员管理端用户审核商家入驻、治理违规商品、查看全平台交易统计数据业务流程可以概括成一条主线商家发布预售商品 → 系统生成预售编号并初始化预库存 → 消费者浏览下单 → 下单时校验并扣减预库存 → 支付定金或全款 → 订单进入“待发货”状态 → 商家后台确认发货 → 消费者确认收货 → 订单完成。我在第一次设计这类系统时总喜欢先把表结构铺开后来发现这是本末倒置。订单状态流转没定清楚表建得再花哨后面也是一团乱麻。正确的顺序是先画业务流程图、定义状态机再倒推数据库表结构最后动手写代码。特别是预售订单的状态最少要包含待付款、已付款/待发货、已发货、已完成、已取消、退款中、已退款这几个节点。每个状态之间哪些转换是合法的要提前写进设计文档。千万别等到代码写了一半才想起来状态没定义完整那时候改改联动的地方能把人改崩溃。1.2 技术栈选型与架构分层思路这套系统的技术栈是SpringBoot Vue MySQL MyBatis属于目前Java领域覆盖面最广的前后端分离组合。后端选SpringBoot核心理由是低配置启动和生态成熟。内嵌Tomcat不用单独部署容器一个jar直接跑起来。配合Spring MVC做接口层、Spring事务管理做数据一致性、Spring Security或JWT做认证授权基本覆盖了全流程的业务需求。相比SSH时代的XML满天飞SpringBoot的自动配置确实把开发者从繁琐的配置里解放了出来。持久层选MyBatis原因是SQL可控性强。电商类项目经常需要联表查询、动态SQL、复杂统计MyBatis的XML写法天然适合这类场景。而且对新手来说MyBatis的门槛比MyBatis-Plus高不了多少却能把SQL功底练扎实。举个例子我这里有用例需要按照多个条件动态查询预售商品列表可能是分类、价格区间、预售状态、上架时间等条件任意组合用MyBatis动态SQL的if标签拼接比JPA的规格查询更直白易懂。预算充足的话也可以在MyBatis基础上引入通用Mapper或MyBatis-Plus只把复杂的统计SQL放到XML里管理。前端选Vue主要是组件化开发和响应式数据流天然适配后台管理类系统。配合Vue Router做路由、Vuex或Pinia做状态管理、Axios做HTTP通信再叠一个Element UI的组件库中后台页面基本是搭建式的开发效率。农产品平台的用户端和管理端页面结构差异很大用户端更看重商品展示的层次性管理端更看重表格和表单的交互密度Vue的组件化正好可以把这两类页面拆成独立的组件树互不干扰又共用一套请求层代码。MySQL这边没有太多玄学InnoDB引擎、utf8mb4字符集、事务隔离级别默认的REPEATABLE READ就够用。如果是写毕设或Demo单库单表完全可以支撑。如果业务预估量比较大可以在建表时就考虑按时间或按店铺ID分表但不要过早引入分库分表中间件那是过度设计。1.3 项目目录结构与开发节奏安排前后端分离项目建议在根目录下拆成两个子项目backend和frontend。后端按照Maven标准目录结构组织常见的包划分如下com.farm.preorder ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # MyBatis数据访问层 ├── entity # 数据库实体 ├── dto # 前后端交互对象 ├── vo # 视图对象 ├── config # 配置类跨域、拦截器、全局异常 ├── common # 统一返回结果、常量、工具类 └── exception # 自定义异常前端Vue项目目录就按Vue CLI或Vite的标准结构来views目录按模块划分比如home、products、cart、order、user、admincomponents目录放复用组件api目录统一管理接口请求函数router目录管理路由表store目录管理全局状态。开发节奏上我强烈建议两条线并行推进出原型再进入细节完善。第一步先搭建前后端骨架把登录注册和商品列表两条链路完整跑通形成一个纵向切片。第二步再横向扩展其余模块。这样做的意义是你能尽早发现前后端联调的基础设施问题比如跨域、Token传递、统一返回格式这些问题越早暴露越好。如果先把所有后端接口写完再开始前端开发联调阶段容易变成“查问题比写代码还久”。2. 数据库设计与核心表结构实现数据库设计是预售平台的重中之重因为预售的业务复杂度几乎都沉淀在数据模型里。普通电商的订单和库存模型拿来直接套一定会出问题。比如普通电商下单后直接扣减真实库存而预售系统下单扣的是“预库存”后续备货或发货阶段还可能根据实际产量做二次调整。这块在设计时一定要想清楚。2.1 核心数据模型设计思路整个系统我拆出以下核心表用户表user账号、密码、昵称、手机号、角色类型、创建时间商家表merchant商家主体信息包括联系人、入驻审核状态和用户表是1对1关联商品分类表category分类名称、层级结构最简方案就是单级分类预售商品表product商品名称、主图、详情描述、分类ID、预售价格、定金金额、预库存总量、已售数量、预售开始/结束时间、发货时间、上下架状态购物车表cart用户ID、商品ID、数量、加入时间订单表pre_order订单编号、用户ID、商品ID、商品快照、购买数量、应付总额、定金金额、支付状态、发货状态、物流单号、下单时间、支付时间、发货时间、取消时间订单明细表order_item一个订单包含多个商品时使用简化设计也可以并入订单表字段的具体设计里有几个细节很容易踩坑我单独补充一下。第一个是金额字段不要用int也不用double要用decimal(10,2)。在很多教学项目里金额用浮点数越积越不准后面一毛两分的对不上账很头疼。第二个是状态字段推荐使用tinyint存数字状态代码里定义枚举去映射绝对不要直接存中文状态值。第三个是逻辑删除所有核心表都加一个deleted字段默认为0删除时做update而不做物理删除。尤其是用户和订单后期统计复盘都依赖于历史数据完整。还有一点也是很多人容易忽略的库存相关的字段要区分“预库存总量”和“可售余量”。业务上预售商品数量是可以动态调整的可能第一批产量3000斤商家后来又追加了500斤。如果表里只有一个库存字段调整时直接覆盖历史数据就失真了。我实际开发中会把库存字段拆成total_stock预库存总量、sold_stock已售数量、remaining_stock当前可售数量后两个字段可以在写操作时通过事务更新。查询列表直接用remaining_stock做条件过滤不做运算性能也更好。2.2 核心建表语句示例直接给出一份精简但可运行的核心建表SQL方便你对照理解。这里略去外键约束外键逻辑全部下沉到Service层因为互联网场景下数据库外键会影响插入和更新的性能而且跨表操作时会增加死锁概率。实际开发中我也是这么做的电商类项目99%不建物理外键。-- 用户表 CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT 用户名, password varchar(128) NOT NULL COMMENT 密码加密存储, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色0消费者 1商家 2管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, deleted tinyint(4) NOT NULL DEFAULT 0 COMMENT 逻辑删除, 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_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 预售商品表 CREATE TABLE product ( id bigint(20) NOT NULL AUTO_INCREMENT, merchant_id bigint(20) NOT NULL COMMENT 商家ID, category_id bigint(20) DEFAULT NULL COMMENT 分类ID, product_name varchar(128) NOT NULL COMMENT 商品名称, product_image varchar(255) DEFAULT NULL COMMENT 主图URL, detail text COMMENT 商品详情富文本, price decimal(10,2) NOT NULL COMMENT 预售总价, deposit decimal(10,2) DEFAULT 0.00 COMMENT 定金金额, total_stock int(11) NOT NULL DEFAULT 0 COMMENT 预库存总量, sold_stock int(11) NOT NULL DEFAULT 0 COMMENT 已售数量, remaining_stock int(11) NOT NULL DEFAULT 0 COMMENT 可售余量, pre_start_time datetime DEFAULT NULL COMMENT 预售开始时间, pre_end_time datetime DEFAULT NULL COMMENT 预售结束时间, delivery_time datetime DEFAULT NULL COMMENT 预计发货时间, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态0草稿 1预售中 2售罄 3下架, deleted tinyint(4) NOT NULL DEFAULT 0, 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_merchant_id (merchant_id), KEY idx_status (status), KEY idx_pre_end_time (pre_end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预售商品表; -- 订单表 CREATE TABLE pre_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, user_id bigint(20) NOT NULL COMMENT 用户ID, product_id bigint(20) NOT NULL COMMENT 商品ID, product_name varchar(128) NOT NULL COMMENT 商品名称快照, product_image varchar(255) DEFAULT NULL COMMENT 商品图片快照, price decimal(10,2) NOT NULL COMMENT 成交单价快照, quantity int(11) NOT NULL DEFAULT 1 COMMENT 购买数量, total_amount decimal(10,2) NOT NULL COMMENT 应付总金额, deposit decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 定金金额, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 支付状态0待支付 1已付定金 2已付尾款 3已退款, order_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 订单状态0待付款 1待发货 2已发货 3已完成 4已取消 5售后中, consignee varchar(64) DEFAULT NULL COMMENT 收货人, consignee_phone varchar(20) DEFAULT NULL COMMENT 收货电话, consignee_address varchar(255) DEFAULT NULL COMMENT 收货地址, delivery_no varchar(64) DEFAULT NULL COMMENT 物流单号, remark varchar(255) DEFAULT NULL COMMENT 用户备注, pay_time datetime DEFAULT NULL COMMENT 支付时间, delivery_time datetime DEFAULT NULL COMMENT 发货时间, finish_time datetime DEFAULT NULL COMMENT 完成时间, cancel_time datetime DEFAULT NULL COMMENT 取消时间, 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_product_id (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预售订单表;2.3 字段设计与索引优化细节建表时有一个很容易被忽略的性能问题就是索引的建立不是越多越好。比如订单表很多人习惯给所有可能查询的字段都加一个索引结果插入速度被拖慢索引占用的空间也很大。正确做法是对照实际查询场景把常用的查询条件抽出来单独建索引。像这个系统里订单列表基本只会以“用户ID”或“商家ID”为维度查询偶尔会按订单号精确查所以user_id、product_id、order_no这三个索引就足够了。如果业务中需要频繁按create_time做时间区间统计可以再考虑联合索引(user_id, create_time)。索引要用在实际高频查询上不是用来给数据库管理工具展示的。还有一个设计细节值得展开订单表里为什么要冗余product_name和product_image字段。因为商品信息是会被修改的商家可能改了价格可能换了主图但用户订单展示的必须是下单那一刻的商品信息。如果订单表不做快照全链路通过product_id关联商品表查询商家后面改了商品标题或图片历史订单展示就会跟着变这在账务上是很不严谨的行为。我曾经见过一个电商项目因为没做商品快照商品下架之后订单详情直接显示空白后面不得已又补了一张下单快照表来修复。时间字段也建议统一处理。MySQL中datetime和timestamp的取舍其实很简单需要区分时区且范围要求高选timestamp纯粹记录业务时间datetime更直观。这个项目里我用datetime避免2038年问题。所有表统一加上create_time和update_time并由数据库维护默认值比在Java代码里显式set要稳妥得多。3. 后端核心功能实现SpringBoot MyBatis全流程后端是整个系统的中枢所有业务规则的校验、数据处理、事务管理都集中在这一层。下面按照模块拆开讲从项目初始化到最后的核心接口实现全部是测试过可以正常跑通的经验方案。3.1 SpringBoot项目初始化与核心配置创建SpringBoot项目没什么秘密直接用IDEA的Spring Initializr。需要注意的坑是版本选择不要盲目追求新版本SpringBoot 2.7.x配合Java 8依然是目前兼容性最稳的组合JDK17SpringBoot3的编译方式和依赖兼容性对新手并不友好。如果是为了跑通教学项目直接用Java 8 SpringBoot 2.7.x MyBatis 3.5.x这一套经典搭配。pom.xml里的核心依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies数据库连接池我这里选了Druid而不是HikariCP主要看中它的监控页面和SQL慢查询日志做教学项目时方便排查问题。如果你对性能有偏执HikariCP更强但Druid对新手更友好。application.yml中的关键配置如下其中druid的监控页要是用不到可以不加server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/farm_preorder?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.farm.preorder.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case这个配置你必须打开否则数据库字段product_name映射到Java属性productName会全部变成null。这个是新手最容易踩的坑没有之一。我第一次用MyBatis时就因为漏了这个配置排查了大半天才知道结果集映射问题。登录认证部分推荐用JWT流程是登录成功生成Token返回前端前端存储Token并在后续请求头中携带后端通过拦截器解析Token并注入用户信息。Spring Security在使用上对新手很不友好配置链复杂我个人建议直接用拦截器ThreadLocal模拟一套轻量级的登录鉴权虽然简洁但核心逻辑和Spring Security完全一致还更容易在答辩时讲清楚。3.2 统一返回格式与全局异常处理后端接口必须统一返回结构不能让一个接口返回JSON对象、另一个返回字符串、第三个直接抛异常页面。我定义了一个轻量的ResultT结构Data public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.setCode(200); r.setMsg(success); r.setData(data); return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.setCode(code); r.setMsg(msg); return r; } }全局异常处理用RestControllerAdvice这样数据校验异常、业务异常、系统异常分别返回不同的错误码。业务上自定义一个BizException在Service层校验失败时直接throw比如“库存不足”“预售未开始”“订单不属于当前用户”等场景。3.3 MyBatis Mapper层与动态SQL实战Mapper接口最简单的写法是直接注解SQL简单查询够用。但电商项目的复杂查询比如后台管理端的筛选列表条件组合非常多强烈建议用XML文件。下面以一个商品列表分页条件查询为例这是整个系统最常用的接口之一select idselectProductList resultTypecom.farm.preorder.entity.Product SELECT id, product_name, product_image, price, deposit, total_stock, sold_stock, remaining_stock, pre_start_time, pre_end_time, delivery_time, status FROM product where deleted 0 if teststatus ! null AND status #{status} /if if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (product_name LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND price gt; #{minPrice} /if if testmaxPrice ! null AND price lt; #{maxPrice} /if /where ORDER BY create_time DESC /selectwhere标签会自动处理掉第一个条件前面的AND这个技巧让SQL拼接非常干净放心保留后面的AND不会出错。这里需要注意的是XML中大于小于号要用gt;和lt;转义否则XML文件在启动解析阶段就会直接报错连系统都跑不起来。另外模糊查询需要用CONCAT拼接而不是直接写LIKE %#{keyword}%因为参数占位符在字符串字面量中不会被解析。分页这块最简单的是PageHelper插件用法是在查询方法前调用PageHelper.startPage(pageNum, pageSize)然后Mapper查询返回的List后面再接一个PageInfo包装。插件原理是通过ThreadLocal把分页参数传给拦截器拦截器在执行SQL前自动拼接LIMIT语句同时发出COUNT语句查询总量。Service public class ProductServiceImpl implements ProductService { Autowired private ProductMapper productMapper; Override public PageInfoProduct queryProductPage(ProductQueryDTO queryDTO) { PageHelper.startPage(queryDTO.getPageNum(), queryDTO.getPageSize()); ListProduct productList productMapper.selectProductList(queryDTO); // 顺便处理一下商品主图完整路径之类的前端展示字段 return new PageInfo(productList); } }3.4 核心业务实现下单与预库存扣减下单接口是整个系统最核心、也最容易出并发问题的地方。基本逻辑分几步校验商品是否存在且处于“预售中”状态校验当前时间是否在预售时间范围内校验购买数量是否大于可售余量生成订单状态为“待付款”扣减预库存增加已售数量这里的关键在于“库存校验扣减”必须是一个原子操作。如果先查库存再在Java代码里判断最后再执行UPDATE一旦高并发同时进来自查到的都是“有库存”但真实库存实际只够一单就会产生超卖。经典解决方案有两种方案一乐观锁扣减库存update iddeductStock UPDATE product SET remaining_stock remaining_stock - #{count}, sold_stock sold_stock #{count} WHERE id #{productId} AND remaining_stock gt; #{count} AND deleted 0 /update这条SQL依靠remaining_stock #{count}条件保证不会扣成负数如果影响行数为0说明库存不足下单失败。这是最简方案也是MyBatis面试题里经常问到的“数据库层面防止超卖”的原始答案。普通毕设和中小业务场景这个方案完全够用。方案二悲观锁SELECT FOR UPDATESELECT id, remaining_stock FROM product WHERE id #{productId} FOR UPDATE;开启事务后先锁住商品行再判断并更新。这种方式在并发控制上更严格但拿锁期间其他事务对该商品的读写都会阻塞对数据库压力更大。实际复杂电商系统中这种直接锁表行的做法也不是主流方案但对一个预售平台来说摆脱了“改了哪里都不知道”的焦虑写代码时思路非常清晰。支付宝沙箱支付、微信支付Native、或者干脆模拟支付点击按钮直接把订单状态改成已支付都可以。我建议毕设或练习项目直接用模拟支付一方面避免企业资质申请的问题一方面把精力留在核心业务而不是支付回调的理解上。在支付模拟接口中注意使用分布式事务不好实现的场景下先更新本地订单状态再包装一层对外通知接口就够了。3.5 订单状态管理与事务控制实践订单状态机是整个业务系统的中枢代码层面要把状态转换封装到独立的Service方法中不能让每个Controller方法都随意修改状态。我实际设计时用了一个OrderStatus枚举public enum OrderStatus { UNPAID(0, 待付款), PAID(1, 待发货), SHIPPED(2, 已发货), COMPLETED(3, 已完成), CANCELED(4, 已取消), AFTER_SALE(5, 售后中); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } }所有状态变更逻辑都收敛在OrderService中比如支付成功后只能从待付款变成待发货发货后只能从待发货变成已发货。如果后面需要扩展售后模块状态机的维护也只需要在一个类里改动。这个设计对系统的长期维护意义非常大避免了下单支付改一个地方、取消订单又改一个地方的状态混乱。事务的控制用Transactional就足够了。这里的要点是项目里一定不要出现事务方法内部的“异步补偿”逻辑。比如把冻结库存的步骤放在一个事务里后面发现某个操作失败就可能在事务回滚之前把补偿任务提交出去了导致库存莫名消失。我踩过一次类似坑之后凡是出现调用外部系统或不确定耗时的操作都要求先完成本地事务再通过消息或定时任务去处理后续流程。单元测试时也记得加上事务回滚注解避免测试数据污染数据库。4. 前端Vue实现从项目初始化到页面交付前端这块的技术点是Vue Router Axios Vuex/Pinia Element UI的组合。对于管理类系统来说Vue的组件化和数据响应式机制在列表页、表单页、状态联动场景有天然优势。下面把从初始化到功能实现的完整流程过一遍。4.1 Vue项目创建与环境配置创建项目前先确认Node环境。前端Vue CLI项目用npm install -g vue/cli安装脚手架或者直接使用Vite创建Vue3项目。Vite的启动速度快很多Vuex在Vue3中对应Pinia状态管理API略有差异思路是相通的。我这边用的是Vue2 Element UI的组合因为网上现成的admin模板、表格表单组件、文档案例最丰富遇到问题随便一搜都有答案适合赶进度的人。Vue3 Element Plus的组合当然更现代但如果时间紧优先选自己最快能写成型的。# 创建Vue项目 vue create farm-frontend # 安装核心依赖 npm install axios vue-router3 vuex3 element-ui2.15.13Element UI可以全量引入开发方便打包体积大一点。体积敏感时改成按需引入用babel-plugin-import插件把组件和样式按需加载。对毕设项目来说全量引入完全没问题。4.2 Axios请求封装与拦截器设计前后端交互最核心是请求层的统一封装。Axios实例配置baseURL指向后端地址timeout设置合理的超时时间然后通过请求拦截器附带JWT Tokenimport axios from axios; import { Message } from element-ui; import router from /router; const service axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器附加token service.interceptors.request.use( config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] Bearer token; } return config; }, error { return Promise.reject(error); } ); // 响应拦截器统一处理状态码 service.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.msg || 请求失败); if (res.code 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(new Error(res.msg)); } return res.data; }, error { Message.error(error.message || 网络异常); return Promise.reject(error); } ); export default service;这里有一个很实用的细节。开发环境下前端跑在8081端口后端跑在8080端口跨域问题不可避免。最简单的解决方式是在Vue项目的vue.config.js中配置devServer代理不需要改后端代码const { defineConfig } require(vue/cli-service); module.exports defineConfig({ devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } });这样前端的/api/user/login请求会被代理转发到后端的/user/login浏览器中的请求地址始终是同源的干净利落。但要注意这只解决了开发环境的跨域生产环境部署时还需要用Nginx反向代理做同样的事情。4.3 核心页面实现商品列表与详情商品列表页是全平台流量的入口重点展示前端对异步数据的处理能力。我在商品列表页中用的是el-card再配网格布局。Vue组件中核心逻辑是页面加载时调用fetchProducts()方法通过Axios请求后端分页接口拿到数据后渲染到模板中。筛选条件变化时重新拉取列表这里要注意防抖处理避免用户切换分类或输入搜索词时连续触发大量请求。商品详情页比列表页复杂的地方在于它要展示预售流程的完整信息预售时间、发货时间、定金和尾款、当前可售余量、倒计时状态等。页面中要动态判断商品状态如果是“预售中”显示下单按钮和倒计时如果还没开始显示“即将开售”如果已售罄按钮直接置灰。做到这个精细程度前端和后端的联动才是完整的。4.4 购物车与下单流程实现购物车设计简单只存用户ID、商品ID和数量。加入购物车时调用后端接口插入记录购物车页面加载时根据商品ID批量查询最新价格和库存。为什么下单时还要从后端重新拿价格而不是直接用前端购物车里的价格因为价格可能已经变了前端数据不可信下单金额必须以后端查询到的实时价格为准。下单前在前端再次校验数量不能超过可售余量但真正的并发控制还是依赖后端做前端校验只是提升用户体验的手段。下单页面将收获信息、商品ID、数量、备注等信息通过POST提交后端返回订单ID和支付ID前端随后跳转到一个模拟收银台页面。点击“确认支付”按钮后调用模拟支付接口成功后刷新订单状态并跳转到订单详情页。整个流程走下来前后端交互的链路就完整贯通了。4.5 路由设计与权限控制前端路由按模块分组典型的映射关系为const router new VueRouter({ routes: [ { path: /login, component: LoginView }, { path: /, component: LayoutView, children: [ { path: home, component: HomeView }, { path: products, component: ProductListView }, { path: products/:id, component: ProductDetailView }, { path: cart, component: CartView }, { path: orders, component: OrderListView }, { path: profile, component: ProfileView } ]}, { path: /admin, component: AdminLayoutView, children: [ { path: products, component: AdminProductView }, { path: orders, component: AdminOrderView } ]} ] }); // 路由守卫未登录跳转登录页 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path ! /login !token) { next(/login); } else { next(); } });路由守卫的next()逻辑要特别注意不要写成多分支都调用next的方式尤其是配合异步请求判断用户权限的场景容易出现重复导航问题。如果只是判断本地是否有Token直接同步判断就够。5. 项目部署与常见问题排查部署方式分两种前后端分离部署和一键打包部署。教学项目和企业实际场景最大区别在于你是否需要一套环境自动化发布流程。如果只想让项目顺利跑演示前后端放到一台服务器即可如果想练一练生产部署可以用Docker Compose把MySQL、Redis如果有、后端jar包、前端Nginx容器编排在一起一条命令拉起整个平台。5.1 本地启动与生产部署步骤后端打包部署# 在backend目录下 mvn clean package -DskipTests # 生成target/farm-preorder.jar nohup java -jar target/farm-preorder.jar --server.port8080 app.log 21 前端构建部署cd farm-frontend npm run build # 生成dist目录将dist内容复制到Nginx的html目录即可Nginx的关键配置是将/api路径代理到后端SpringBoot同时将前端路由指向index.html并且配置try_files以保证前端路由模式下刷新页面不出现404。这个404问题几乎是每位用Vue Router历史模式的人都会踩的坑原因在于刷新时浏览器直接请求了当前路径Nginx找不到对应的文件于是返回404。配置如下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; } }需要注意proxy_pass后面如果带了/会把/api前缀去掉再转发给后端类似于开发环境代理的pathRewrite效果。如果不带/就是原样完整路径转发。很多人的前后端联调问题都出在这一句话的差别上。5.2 高并发超卖问题的处理方案对比之前提到的乐观锁方案虽然简单但遇到极端并发场景同一秒几千人抢购同一款预售农产品性能会下降。更贴近生产的方案是引入Redis做分布式锁。库存本身也建议放在Redis中用Lua脚本原子扣减扣减成功后再异步刷新到MySQL。这样做的优点是Redis单线程模型的原子性天然解决超卖缺点是系统复杂度上升需要处理缓存和数据库的最终一致性。如果只是做教学项目我强烈建议先用数据库乐观锁方案。原因在于分布式锁、缓存一致性、延误消息等中间件问题会大幅分散你的精力真正的核心业务反而被边缘化了。把乐观锁讲清楚并说明设计思路就已经达到项目练习的目的。5.3 高频报错排查速查表整理一份我在开发中高频遇到的报错场景和对应解法非常实用报错现象根本原因快速排查/解决数据库连接失败URL配置错误或驱动不匹配检查MySQL版本8.x必须用com.mysql.cj.jdbc.Driver实体类属性全为null未开启驼峰映射map-underscore-to-camel-case: trueSQL语法异常XML报错XML中未转义大于小于号检查是否使用gt;和lt;前端请求404跨域/代理配置错误确认API路径和代理转发的对应关系前端刷新页面404Nginx没有配置try_files增加try_files $uri $uri/ /index.html部署后CSS/图片加载异常前端资源路径配置错误Vue项目publicPath改为./后端参数接收为null前端传参格式和后端不一致检查是否需要RequestBody或RequestParam订单状态混乱状态枚举定义不完整用状态机集中管理禁止随手set statusToken失效后接口仍可调用拦截器校验逻辑漏配URL白名单检查排除路径配置是否覆盖了登录和静态资源商品上架后前端列表看不到列表接口过滤了status字段确认查询条件中deleted和status的判断逻辑5.4 部署环境变量与配置文件分离生产环境和开发环境的数据库地址、端口、日志级别基本都不同最稳妥的方式是通过SpringBoot的profile机制区分。在application.yml同目录下创建application-dev.yml和application-prod.yml启动命令添加--spring.profiles.activeprod即可。另外绝对不要把数据库密码等敏感信息硬编码在代码中虽然教学项目大家图省事通常直接写但养成用环境变量注入的习惯对你的职业发展会很有帮助。前端方面不同环境使用不同的API地址可以通过环境变量文件来管理在项目根目录创建.env.development和.env.production里面定义VUE_APP_BASE_URL这类变量。记住以VUE_APP_开头的变量才会被Vue CLI暴露给客户端代码。6. 项目优化方向与个人实践经验一个系统能跑通只是开始能够说明白哪里可以优化、怎么优化才是真正体现价值的地方。这个项目可以作为起点向几个方向扩展演进。6.1 技术层面的可扩展点Redis缓存商品详情页和首页的热门商品列表访问量大可以把查询结果缓存到Redis设置合理的过期时间比如30分钟减少数据库压力。用户登录Token也可以存到Redis中实现后端主动失效避免JWT被劫持后无法提前撤销的问题。消息队列支付成功后的异步通知、发货后的短信提醒、秒杀场景下的限流削峰都可以引入RabbitMQ或RocketMQ来解耦。比如用户下单成功后系统只需要发送一条消息给订单服务去更新状态不需要同步等待发货流程执行完成。分布式事务如果后续把订单、库存、支付拆到不同服务数据一致性就不能靠本地事务解决了。可以研究一下Seata的AT模式或者TCC模式但需要明确这些方案引入的成本和运维负担是很大的不要为了分布式而分布式。6.2 业务扩展方向农产品预售平台在真实业务中通常还带产地溯源和物流追踪功能。产地溯源可以通过接入第三方服务将产地信息、检测报告、采摘批次信息关联到商品详情页物流追踪对接快递鸟或顺丰API由发货操作触发物流轨迹的实时更新。这两个功能都很贴近农产品场景做成功能亮点写进项目介绍里会很有说服力。营销侧的优惠券、限时折扣、拼团预售等功能也是实际电商平台的高频需求。做的时候先设计好数据模型比如优惠券表、用户优惠券表然后在下单计算金额时加入抵扣逻辑。这种纵向扩展能让你把设计模式、规则引擎这些知识点串起来。6.3 最后想说的实操体会这个系统我完整搭建过前前后后大概花了四周。踩过最大的坑不是技术本身而是需求边界没有定清楚就开始写代码导致后面返工几次。整个项目做完我自己的一个习惯是任何项目启动前先花半天把所有角色、所有状态流转、所有页面用文档梳理出来之后开发过程中随时回头查对这种投入非常值得。如果你今年准备毕业设计答辩或者要往简历里放项目别只停留在“代码能跑”的层面先把系统的三张图吃透业务流程图、数据库ER图、系统架构图。这三张图讲清楚了面试官对你的认可度会上升一个台阶。实际操作时多留时间测试和整理文档因为写代码只占项目时间的60%剩下40%都在解决环境问题、联调问题和演示意外。项目源码里还附带了完整的数据库初始化脚本和接口文档拿到之后先从README开始看把数据库跑起来再启动后端最后启动前端按这个顺序就能完整跑通。如果过程中遇到问题绝大多数都能在上面的排查速查表里找到答案。
返回列表