基于Spring Boot+Vue的农产品销售管理系统设计与实现 1. 这个项目是从哪来的它解决了什么问题两年前接了个私活帮一个做蔬菜配送的老板做一套线上销售系统。当时他还在用Excel记账每天早上的订单靠微信语音接中午再手动录入经常出现客户下了单、仓库不知道、送货单填错数量这种问题。做了一圈调研之后我决定用Java Vue来做这套农产品销售管理系统后端负责业务逻辑和数据处理前端做后台管理和零售页面最终交付给客户的除了可运行的源码还有一张完整的设计图和一份部署文档。这类系统在电商管理系统里属于非常典型的骨架型项目。它没有特别复杂的算法但把用户管理、商品管理、订单流程、库存扣减、销售统计这些业务全都覆盖到了。也正因为如此它特别适合几种人计算机相关专业的学生拿来当毕业设计或者课程设计刚学完Spring Boot和Vue的开发者想找一个完整的全栈项目练手中小型农产品企业、合作社想要快速搭建自己的销售后台接外包或做毕设辅导的人需要一套结构清晰、能二次开发的底子。这套系统的核心价值不在于代码有多高级而在于它把农产品的特殊业务链路梳理清楚了。农产品和普通标品不一样生鲜有保质期价格会随行情波动库存可能一晚上就变订单必须快速流转。如果只是照抄一个通用电商系统往往没法用。这也是我后来做系统设计时最花心思的地方。1.1 农产品销售业务比普通电商复杂在哪大部分电商系统的商品是永久库存上架了就能一直卖。农产品不行今天摘的菜明天卖不掉就得打折处理后天可能就要报废。所以系统里必须要有批次和新鲜度的概念。我在设计商品表时不仅存库存数量还要存生产日期、保质期、单位斤/箱/件、是否允许预售这些字段直接影响订单处理和库存扣减的逻辑。另外农产品的价格波动很大。同一个品种今天批发价两块明天可能两块五。系统里如果只有原价和折扣价两个字段根本应付不了。我在价格设计上做了三档市场参考价、农户供货价、销售价。管理员和农户各自维护自己的价格维度前端展示销售价后台统计时用供货价计算利润。这个设计在后面的报表模块帮了大忙能直接算出毛利润而不是只能看出销售额。1.2 这套系统的完整交付形态标题里说的源码数据库文档其实代表了一个项目从开发到交付的三个关键资产缺一不可。源码不用多说是整个系统的躯干数据库决定了业务数据怎么组织文档则是让别人能接手、能扩展的说明书。我按这个思路把项目分成了三部分后端工程基于Spring Boot 2.7.xJava 8使用MyBatis-Plus操作MySQLJWT做登录认证前端工程基于Vue 2.6 Element UI使用Vue Router和Vuexaxios发请求数据库脚本init.sql包含建库建表语句data.sql预置了管理员账号、测试商品和演示订单。这样的组合对目标用户来说足够主流遇到问题能搜到大量参考案例不至于卡在一个冷门依赖上大半天。后面我会逐个部分展开讲。2. 技术选型分析为什么固定用Java Vue而不是别的组合说到技术选型很多人第一个反应是为什么不用Spring Cloud为什么不用React我想说任何技术选型都要先看场景。这套系统的定位是中小型业务管理系统高并发、分布式这些词跟它没关系真正要的是开发效率高、后续维护容易、能找工作或交作业的时候被认可。2.1 后端选Spring Boot的三个现实理由第一生态太成熟了。Spring Boot把配置自动化的程度做得非常高一个starter依赖就能集成MyBatis、Redis、邮件、定时任务等。做管理系统90%的功能都围绕着增删改查Spring Boot在这方面的最佳实践几乎成了行业标准照着写就能少踩很多坑。第二招人和学习资料都充足。如果这个项目以后要交给别人维护Java后端的开发者基数大代码风格也相对统一。对做毕设的学生来说Spring Boot的教程和踩坑记录全网都是遇到Whitelabel Error Page这类问题搜索一下就能定位到原因。第三Spring生态里的Spring Security、MyBatis-Plus、Lombok这些配套工具能极大减少重复代码。比如MyBatis-Plus的分页插件一行代码就能搞定分页查询Lombok的Data注解省掉了大量getter/setter。这些对提升开发效率非常明显。2.2 前端选Vue的核心理由Vue在国内管理系统的普及率非常高尤其是Vue 2 Element UI这套组合几乎是为后台管理系统量身定做的。Element UI提供现成的表格、表单、弹窗、分页、树形控件写完一个页面的时间如果用原生JS写可能连样式都调不完。Vue本身的响应式机制和组件化思想让页面代码非常容易被理解。每个页面就是一个.vue单文件组件模板、脚本、样式放在一起新人接手时打开文件就能看出这个页面在干嘛。配合Vue Router做路由跳转、Vuex做全局状态管理三者的分工非常清晰Vue Router管理页面路由包括登录后的权限路由Vuex存放用户信息、token、购物车数据这类全局共享的数据axios负责HTTP请求统一在拦截器里处理token注入和错误提示。2.3 坦率地说一下这套组合的成本Java Vue并不是学习成本最低的组合。如果只是想快速跑通一个demoPHP或者Node.js可能更快。但考虑到这是一个要交付、要演示、可能会被拿去面试讲解的系统Java Vue的认可度是很重要的。面试官看到简历上写基于Java Vue的农产品销售管理系统会默认你对主流前后端分离开发有一套完整的理解这正是课程设计或毕设想考察的能力。而且Java后端对数据规范、事务控制、权限模型有更好的表达能力。农产品销售涉及钱、库存、订单状态流转这些场景里MySQL事务和Spring的声明式事务是必须掌握的。换成脚本语言可能写起来更快但事务边界和异常回滚的逻辑容易被忽略。3. 功能模块拆解从用户登录到销售统计的完整链路做系统设计时我没一上来就写代码而是先画了一张功能脑图把整个系统需要覆盖的业务场景全部列出来再逐层细化。这里我把最终版的功能模块拆出来每个模块都包含了业务逻辑和页面实现两个层面。3.1 用户与权限模块三种角色各管各的事农产品销售系统里用户的身份不能只有管理员和普通用户两种因为农户和消费者的操作完全不一样。我设计了三个角色角色核心操作页面权限管理员审核商品、管理分类、查看所有订单、销售统计、管理公告、管理用户全部后台页面农户/供应商发布商品、修改自己商品的库存和价格、处理自己的订单、查看自己的销售数据商品管理、订单管理、我的统计消费者/客户浏览商品、加入购物车、下单、查看订单状态、编辑个人信息商城页面、购物车、个人中心权限控制是用JWT 后端拦截器实现的。登录成功后后端签发一个token里面存用户id和角色前端每次请求在请求头里带token后端通过拦截器解析token再把用户信息放入ThreadLocal供Controller层直接获取。这样接口层代码不需要每个都写从session里拿用户干净很多。3.2 商品管理模块好商品是管出来的商品模块我分了几个独立的子功能商品分类管理支持二级分类比如蔬菜 叶菜类这样前端商城页可以按分类筛选商品信息管理维护商品名称、图片、规格、单位、产地、生产日期、保质期、供货价、销售价、库存量、上下架状态库存预警当库存低于预警阈值时商品列表高亮显示并且统计模块里有一个库存预警清单批量操作支持批量上架、批量下架、批量改价。农产品价格变化频繁如果一个一个改会累死运营人员。商品图片的处理我没有做复杂的文件服务器而是把图片上传到服务器本地目录然后把访问URL存进数据库。系统规模不大时这种做法最省事。如果要扩展到生产环境再换成OSS或者MinIO对象存储代码里只需要改上传工具类。3.3 订单模块状态机是订单系统的灵魂订单是这个系统里业务逻辑最重的部分。消费者的操作路径是浏览商品 - 加入购物车 - 提交订单 - 模拟支付 - 等待发货 - 确认收货。农户端看到订单后可以修改订单状态为已发货并填写物流单号。订单状态我用一个整数字段保存并且在代码里定义了常量类0待付款1待发货已支付2待收货已发货3已完成已收货4已取消5退款/售后为什么要用数字而不是直接存中文因为数据库里存中文字符串不稳定容易有编码问题而且后期如果想加状态中文枚举值会显得很乱。数字状态配合状态名映射再在VO视图对象里翻译成文字是更规范的做法。这里要重点说一下取消订单的库存处理逻辑。用户如果在待付款状态下取消订单系统要自动把商品库存加回去。这个操作必须和订单状态更新放在同一个事务里否则会出现订单取消了、库存却没恢复的问题。3.4 统计与报表模块让数据变成经营决策统计模块一开始我做得比较简单就是一个商品销量排行。后来客户说要能看出每天的利润我才加了更多维度的统计。最后做出来三个页签销售趋势按天/按月统计订单数量和销售额用折线图展示商品排行按销量、销售额、毛利润分别排名用柱状图展示经营概况今日订单数、今日销售额、本月订单数、本月销售额、库存预警数量等核心指标聚合到一张卡片面板上。图表我用了ECharts它和Vue的整合很成熟。后端提供的是聚合查询接口比如查询最近30天每天的订单总额用MySQL的DATE_FORMAT函数按天分组一次SQL就能查出来前端拿数据直接画图性能完全够用。4. 数据库设计表结构规划得当才能少走弯路数据库设计是这个项目里最不能省时间的环节。我见过很多人一上来就建表结果做到订单模块发现表结构不够用再回头改前端页面、后端接口全都要跟着动相当痛苦。我复盘了一下自己的数据库设计核心表一共十张每张表的设计都有明确的目的。4.1 核心表结构一览先放一张最终的表清单方便大家对照理解表名作用关键字段sys_user用户表id, username, password, real_name, role, phone, statusproduct_category商品分类表id, parent_id, name, sort_orderproduct商品表id, category_id, name, image, unit, price, supply_price, stock, warning_stock, shelf_status, produce_date, shelf_lifecart购物车表id, user_id, product_id, quantity, checkedorders订单主表id, order_no, user_id, farmer_id, total_amount, pay_status, order_status, receiver_name, receiver_phone, receiver_address, create_timeorder_item订单明细表id, order_id, product_id, product_name, product_image, price, quantity, subtotaladdress收货地址表id, user_id, receiver_name, receiver_phone, province, city, district, detail, is_defaultnotice公告表id, title, content, create_timeoperation_log操作日志表id, user_id, action, detail, create_timestatistics_daily每日统计表id, stat_date, order_count, total_amount, profit, product_count用户表里没有做单独角色表因为系统只有三种固定角色用一个role字段存字符串ADMIN/FARMER/USER就够了。引入RBAC角色表、菜单表、角色菜单关联表会显得很专业但如果业务场景用不到动态权限配置反而增加复杂度。4.2 订单主表与订单明细表为什么必须分开这是我在设计时特别注意的一点。很多人入门做订单时喜欢直接在订单表里存一个商品名字段下单时把所有商品塞成一个字符串。这样做的问题在统计阶段会暴露出来——你想按商品维度统计销量时发现只能靠遍历字符串来拆效率低下且容易出错。正确的做法是订单主表和订单明细表一对多关联。订单主表只存订单的公共信息用户、总金额、状态、收货人、时间订单明细表存每一个商品的购买记录商品id、下单时快照的名称和图片、单价、数量、小计。这里有一个细节值得注意明细表里除了存product_id还要冗余存一份product_name和product_image。为什么因为商品信息是会变的农户可能今天把红富士苹果改成了烟台红富士苹果甚至下架了。如果明细表只关联product_id查历史订单时商品名就会变成未知商品。只有把下单那一刻的商品信息存成快照历史订单才能完整还原。4.3 库存扣减如何避免超卖问题库存是另一个设计容易翻车的地方。最简单的实现是先查库存判断够不够再UPDATE stock stock - 1但在高并发场景下会超卖。农产品系统的并发量没有电商大促那么夸张但下单瞬间库存被扣成负数这种基础错误绝不该出现。我采用了两个手段使用UPDATE语句做原子操作UPDATE product SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}受影响行数为1才表示扣减成功在订单创建的Service方法上加Transactional确保扣库存、生成订单、生成明细三个操作要么全部成功要么全部回滚。如果以后要做秒杀级别的并发可以引入Redis预扣库存或者乐观锁版本号但对这套系统来说当前方案已经足够稳妥。5. 后端开发详解接口设计、权限控制和事务处理后端框架用的是Spring Boot MyBatis-Plus。MyBatis-Plus对单表CRUD的简化幅度非常大BaseMapper内置了selectById、insert、updateById、deleteById这些方法不用自己写SQL。但多表关联查询和复杂统计我还是写了XML里的自定义SQL保证执行效率和可读性。5.1 后端工程目录结构工程目录我习惯按业务模块分包而不是按技术类型分包。这样做的最大好处是一个业务模块的Controller、Service、Mapper能放在同一个包路径下查找代码时顺着模块名字点进去就行。com.farm.sales ├── common │ ├── exception // 全局异常处理 │ ├── result // 统一返回结果封装 │ └── utils // JWT工具、日期工具 ├── config │ ├── CorsConfig // 跨域配置 │ ├── MybatisPlusConfig // 分页插件配置 │ └── WebMvcConfig // 拦截器注册 ├── controller │ ├── AdminController │ ├── UserController │ ├── ProductController │ ├── CategoryController │ ├── CartController │ ├── OrderController │ ├── NoticeController │ └── StatisticsController ├── entity │ ├── User.java │ ├── Product.java │ ├── Order.java │ └── ... ├── mapper │ ├── UserMapper.java │ ├── ProductMapper.java │ └── ... ├── service │ ├── impl │ └── ... └── vo ├── LoginVO.java ├── OrderDetailVO.java └── ProductVO.java5.2 一个完整的登录接口从Controller到Mapper登录接口虽然简单但它把整条链路串起来了。我用代码示例来说明RestController RequestMapping(/api/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { String token userService.login(dto.getUsername(), dto.getPassword()); return Result.success(token); } }Service层主要负责校验逻辑public String login(String username, String password) { LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.eq(User::getUsername, username); User user userMapper.selectOne(wrapper); if (user null) { throw new BusinessException(用户名不存在); } // password在入库时用MD5加盐存储这里做比对 String encrypted MD5Utils.md5(password user.getSalt()); if (!user.getPassword().equals(encrypted)) { throw new BusinessException(密码错误); } if (DISABLED.equals(user.getStatus())) { throw new BusinessException(账号已被禁用请联系管理员); } // 生成JWT有效期24小时 return JwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole()); }这里加盐的细节很多人会忽略。直接MD5存储密码容易被彩虹表破解。我在用户表里加了一个salt字段注册时生成随机字符串保存的密码是MD5(password salt)。虽然不算最安全的BCrypt方案但对教学项目来说体现了不要明文存密码的正确意识。如果要在生产环境用建议换成BCryptPasswordEncoder。5.3 权限拦截登录校验和角色校验分开做我没有引入完整的Spring Security因为对于只有三种角色的系统Spring Security的配置工作反而占了大头。我用Spring MVC的HandlerInterceptor做了一个轻量级拦截器效果完全可以满足需求。拦截器里做两件事从请求头Authorization里取出token解析失败则直接返回401把解析出的用户信息放入ThreadLocal方便Controller和Service层取用。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录、注册、首页商品浏览等公开接口 String uri request.getRequestURI(); if (uri.startsWith(/api/user/login) || uri.startsWith(/api/product/list)) { return true; } String token request.getHeader(Authorization); if (token null || !JwtUtils.verify(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } UserContext.set(JwtUtils.getUserFromToken(token)); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }角色校验我采用了注解方式自定义一个RequireRole(ADMIN)然后在拦截器里扫描HandlerMethod上的注解校验当前用户角色是否匹配。这样在每个接口上标一下就能清楚地表达这个接口是谁能访问的。5.4 统一返回结构和全局异常处理如果每个接口返回的格式都不同前端会写得很痛苦。我从一开始就定死了统一返回结构public class Result { private Integer code; // 200成功400业务失败401未登录500系统异常 private String message; private Object data; }Controller里只要return Result.success(data)或return Result.error(库存不足)就行。全局异常处理器负责兜底捕获业务异常、参数校验异常、系统异常分别返回对应的JSON结构。这样前端axios的响应拦截器只需要判断code就能统一弹出错误提示不需要每个接口单独处理错误分支。6. 前端开发实录Vue Element UI 搭建管理后台和商城页面前端部分是整个系统里看得见摸得着的部分也是演示时最出效果的地方。我把它拆成两个入口一个是给管理员和农户用的后台管理界面另一个是给消费者用的商城页面。两者的前端工程可以放在同一个项目里用路由来区分。6.1 前端目录和路由组织工程初始化用的是Vue CLI目录结构如下src ├── api │ ├── product.js │ ├── order.js │ ├── cart.js │ └── user.js ├── assets ├── components │ ├── Header.vue │ └── UploadImage.vue ├── router │ └── index.js ├── store │ └── index.js ├── views │ ├── admin │ │ ├── Dashboard.vue │ │ ├── ProductList.vue │ │ ├── ProductEdit.vue │ │ ├── CategoryManage.vue │ │ ├── OrderManage.vue │ │ ├── UserManage.vue │ │ └── Statistics.vue │ ├── mall │ │ ├── Home.vue │ │ ├── ProductDetail.vue │ │ ├── Cart.vue │ │ └── Checkout.vue │ └── login │ └── Login.vue ├── App.vue └── main.js后台管理页面的布局用了Element UI的Container布局左侧是菜单栏右侧是主要内容区。路由里嵌套了一个Layout组件所有后台子页面都是它的children。这样菜单和顶栏只需要写一次切换页面时只有右侧内容区刷新。6.2 axios封装请求拦截和响应处理axios封装是前端工程里我最先写的部分。因为所有接口请求都要带token所有错误提示都要统一不封装的话每个页面里重复代码会非常多。import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL, timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) // 响应拦截器统一处理业务码 service.interceptors.response.use( response { const res response.data if (res.code 200) { return res.data } else if (res.code 401) { localStorage.removeItem(token) router.push(/login) Message.error(登录已过期请重新登录) return Promise.reject(new Error(res.message)) } else { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } }, error { Message.error(error.message || 网络异常) return Promise.reject(error) } ) export default service值得留意的是我在响应拦截器里把res.data直接返回了这样业务层拿到的直接就是后端返回的数据体而不是整个响应对象。比如调用getProductList()拿到的是数组或分页对象可以直接赋值给data省去了在页面里写response.data.data.data的尴尬。6.3 权限路由根据角色动态生成菜单管理系统通常希望不同角色登录后看到的菜单不一样。游客登录后是商城页面农户登录后能看到商品上架、不过看不到用户管理。如果用静态路由一次性注册所有页面用户手动改URL也能进没权限的页面。我采用的方案是路由表分为公共路由和动态路由。公共路由包括登录页、商城首页、商品详情、购物车动态路由是一个数组里面每一项配置了对应的角色权限登录后用router.addRoutes按角色注册。const adminRoutes [ { path: /admin, component: Layout, children: [ { path: dashboard, component: Dashboard, meta: { title: 经营概况, roles: [ADMIN, FARMER] } }, { path: product-list, component: ProductList, meta: { title: 商品管理, roles: [ADMIN, FARMER] } }, { path: category, component: CategoryManage, meta: { title: 分类管理, roles: [ADMIN] } }, { path: user-manage, component: UserManage, meta: { title: 用户管理, roles: [ADMIN] } } ]} ]菜单栏组件从router.options.routes里读取当前用户有权限的路由动态渲染成el-menu的菜单项。这样前端菜单、路由权限和后端接口权限形成了双层验证页面层的体验和后端的数据安全都兼顾了。6.4 商品列表和购物车两个最典型的页面商品列表页用到的是Element UI的Table组件。它和分页组件搭配起来非常顺手但要注意一点Table的列数据如果涉及后端枚举值比如状态字段1代表上架最好在表格里用formatter函数翻译而不是直接展示数字。购物车页面是Vuex状态管理的一个好场景。购物车数据在用户刷新后还需要保留所以我在Vuex里维护了一份的同时每次增删改都会调用后端接口同步到数据库。这样刷新页面后从后端重新拉取就不会丢。state: { cartList: [], cartTotal: 0 }购物车选中状态、数量加减、小计金额这些在Vuex的getter里计算会非常清晰。getter可以基于state派生出总金额、总数量模板里直接引用派生属性不需要在每个组件里重复计算。7. 让项目可交付源码组织、数据库脚本和文档编写代码写完了项目只完成了一半。对一个源码数据库文档的交付项目来说整理和包装往往比写代码更体现专业性。接手一个新项目我最先看的不是业务代码而是README、数据库脚本和部署文档。这三样东西做得清晰接手成本能降低一半。7.1 源码目录怎么组织才算规范我用了前后端分离的方式一个总git仓库里放两个子目录farm-sales-system ├── backend // 后端Spring Boot工程 │ ├── src │ ├── pom.xml │ └── README.md ├── frontend // 前端Vue工程 │ ├── src │ ├── package.json │ └── README.md ├── database │ ├── init.sql // 建表 │ └── data.sql // 测试数据 └── docs ├── 需求说明.md ├── 数据库设计.md ├── 接口文档.md └── 部署文档.md每个子项目里有一份简短的README说明这是什么项目、用什么版本、怎么启动。总目录下的README写清楚系统简介、账号分配、启动步骤。这个习惯救过我很多次——有人接手或者答辩的时候直接照着README就能把项目跑起来不用反复问我环境怎么配。7.2 数据库脚本的交付细节初始化脚本最重要的要求是一个脚本执行完数据库就具备完整结构并能跑通演示。我在init.sql里做了几件事按依赖关系先删表再建表先删子表再删主表保证脚本可以重复执行每个字段都加上COMMENT注释字段含义一目了然外键和索引单独建不写在CREATE TABLE里避免后面加索引时要改表结构预置三个角色的测试账号管理员/农户/消费者各一个密码统一为123456但存储的是加盐MD5后的密文。测试数据我在data.sql里预置了十几种常见农产品叶菜类、根茎类、水果类。每一条数据的库存量、价格、生产日期都按真实逻辑填写比如叶菜类的生产日期是昨天保质期是3天这样启动系统后看到的列表和预警提示都符合直觉演示效果自然得多。7.3 文档到底要写哪些内容文档方面我准备了四份分别对应不同场景的读者需求说明给没参与开发的人看讲清楚这套系统要解决什么问题、有哪些角色、核心流程是什么。文档前面要有业务背景后面要有每个模块的功能清单。数据库设计文档给后端开发者看包含ER图说明和每张表的字段说明。表结构如果有修改这份文档要同步维护不然时间一长就会失真。接口文档给前端或第三方对接的人看。我用Swagger自动生成了大部分接口文档运行后端后访问/swagger-ui.html就能看到。另外手动补充了一份核心接口的调用示例特别是登录、下单、统计数据这类关键接口。部署文档给运维或自己以后看写清楚环境要求、JDK/Node/MySQL版本、数据库导入步骤、前端构建命令、后端打包命令、Nginx配置示例、如何放行端口。每一步命令都贴出来不让读者猜。写部署文档有个小技巧每写一步就在一台干净环境的机器上从零执行一遍记录实际执行的命令和输出。不要凭记忆写否则很容易漏掉环境变量配置或者依赖安装步骤。8. 复盘整个开发过程里踩过的坑以及给新手的几条建议说实话这个系统前前后后开发了四个星期真正写业务代码的时间可能两周不到剩下的一半时间都花在改Bug、调环境、整理交付物上。我把这些经历总结一下希望能帮后来者少走一些弯路。8.1 最容易翻车的地方逐一细说跨域问题。前后端分离开发时前端跑在8080端口后端跑在8081端口浏览器会拦截跨域请求。我当时第一反应是在前端配代理vue.config.js里写proxy但上线后Nginx环境不一样又需要后端加CrossOrigin或统一的CorsConfig。建议从一开始就做好后端CORS全局配置同时保留前端代理两套方案分别对应开发和部署两种场景。数据库编码。MySQL建库时默认utf8mb4没选对导致存入emoji表情或者不常用生僻字时报错。项目里收货地址、公告内容都可能有这类字符我后来统一在建库语句里强制指定utf8mb4并且修改了数据库连接URL加上characterEncodingutf8参数。前端响应式数据更新问题。Vue 2的Object.defineProperty无法检测新增属性直接给对象添加一个字段页面不会更新。我在处理订单列表时踩过一次这个坑——从后端拿到的订单对象里没有状态文字这个字段我在代码里动态加了一次页面一直不显示。解决方式是提前在data里声明好完整的字段结构或者用this.$set来添加响应式属性。大金额计算精度。Java的double类型在金额计算上会有误差比如0.1 0.2得到0.30000000000000004。订单金额、利润统计这类场景必须用BigDecimal不能偷懒用double。这个原则我在设计实体类时定了死规矩——所有金额字段一律用BigDecimal前端传来金额也不要直接用double接收。8.2 关于农产品这个业务域我自己的几个教训第一库存单位必须统一。表结构里叫stock单位是斤还是箱很容易搞混。我遇到过农户提交商品时填的数量是500但实际是500箱而不是500斤。后来在商品表里加了一个unit字段而且前端表单在商品数量输入框后面跟一个单位下拉框强制用户选择彻底杜绝了这个歧义。第二价格要区分供货价和销售价。一开始我只做了销售价结果做统计的时候发现没法算利润又回头改表结构、改页面相当痛苦。如果你正在做类似的系统建议设计阶段就把这两个价格字段都加上。第三订单状态流转要克制。别把状态细分到十几二十个状态越多逻辑判断越复杂用户也看不懂。五个状态是我测试下来比较舒服的粒度覆盖了从下单到售后的完整生命周期代码里写switch分支也清晰。8.3 后续可以从哪些方向扩展如果这个项目要继续往深处做我认为有三个方向最有价值增加微信小程序端。农产品销售场景里消费者更多是在手机上完成购买。后端接口已经做成了RESTful风格小程序端直接复用这套接口只需要新写一个前端小程序工程。商品浏览、下单、支付这几个核心链路都比较标准迁移成本可控。引入Redis缓存热点数据。商品首页、分类列表这些接口相对高频缓存到Redis后可以显著减轻数据库压力。另外购物车和用户token都可以考虑放进Redis提高访问速度和敏感数据的可控性。对接物流跟踪和电子面单。做生鲜配送经常要和第三方物流系统对接订单发货后能自动同步物流轨迹消费者在订单详情页就能看到配送进度。这样的功能在真实业务里会非常加分。项目做到这一步功能已经完整代码结构也算清晰。对我来说这项开发最有价值的收获不是掌握了某个框架的某个API而是完整经历了业务分析 — 数据库设计 — 后端开发 — 前端联调 — 文档交付的闭环把零散的知识串成了一条线。如果你也在做类似的Java Vue全栈项目希望这篇复盘能帮你把路走得更顺。