最近在帮一位做家庭农场的朋友梳理会员管理流程他那边最头疼的就是认养订单和周末采摘预约客户在微信群里接龙报名订单信息躺在Excel里经常出现同一天同一块地重复排期、退款记录对不上账的情况。聊过几次之后我干脆把这套业务场景完整做成一个能跑的项目也就是标题里说的乐享田园系统——一个基于SpringBootVue的前后端分离管理系统后端用JavaMySQLMyBatis提供接口前端用Vue负责页面交互覆盖土地认养、体验预约、农产品展示和后台管理这几条核心链路。这套项目适合两类人参考一是刚学完SpringBoot和Vue基础知识想找一个完整业务闭环来练手的人二是做毕业设计或作品集需要一套需求分析—数据库设计—接口开发—前端联调全过程的项目。和常见的单表CRUD Demo不一样它里面有真实业务里才会遇到的状态流转、库存扣减、时间排期和权限控制踩坑的地方也更多反而更有学习价值。下面我从业务建模开始把整个系统的设计思路和实现过程拆开讲清楚。1. 为什么做乐享田园从农场调度乱象到三条业务主线1.1 一个真实的管理痛点先说朋友的农场。他那里有几十块认养菜地客户每年交一笔费用认养其中一块平时由农场代管周末客户可以自己来浇水采摘另外还有一块采摘区按天预约限流。过去全靠人工登记最典型的问题有三个认养订单没有状态概念钱收了但土地是不是还在维护中、合同什么时候到期全凭脑子记。采摘预约按天在群里接龙同一时段人数一多就乱经常到了现场才发现超员。财务对账困难订单是微信转账记录Excel里只有客户名字和土地编号到底哪些订单在有效期内完全看不出来。这些场景提炼出来就是典型的资源型预约订单管理业务系统里有有限的土地资源、排期时段资源用户发起认养或预约动作后台需要校验资源是否可用并在一系列操作后更新资源状态。这正是管理类系统里最有代表性的部分。1.2 功能边界用户端和管理端各管哪些事我从实际使用角色出发把系统分成两个端用户端面向普通访客和认养客户核心操作是注册登录维护个人资料浏览田园动态、认养土地列表、土地详情发起土地认养订单选择认养时长半年/一年/两年预约周末采摘体验选择日期、时段和人数查看自己的订单列表和预约记录管理端面向农场管理员核心操作是维护土地档案增删改查、上下架、设置价格和状态维护采摘排期时段日期、时段、可预约人数审核认养订单处理取消和到期操作发布田园资讯内容查看基本经营数据认养总数、待审核订单数等整套系统的核心不是页面多漂亮而是认养和预约两条业务线的状态闭环。用户端负责发起业务管理端负责审核和资源维护两端共享同一套数据模型状态在操作之间流转这一点在后端设计和数据库建模时会体现得很明显。1.3 我主动砍掉的功能清单做这类项目最容易犯的毛病是需求越加越多最后什么都做不完。我在设计阶段就砍掉了三块内容真实在线支付不接支付渠道下单后走待确认—管理员在后台标记收款的流程。这样既演示了订单状态机又不需要处理支付回调、对账这些外部依赖。多农场多商户所有数据围绕单一农场的运营模型设计不引入商户维度避免表结构复杂度翻倍。物联网设备对接不做温湿度传感器、视频监控这类硬件联动保持纯软件系统。砍需求的目的是让项目聚焦在完整可运行而不是功能堆砌。如果你做毕设把这两条业务线跑通、讲清楚已经比大多数只有增删改查的系统扎实了。2. 技术选型不是抄作业SpringBootVueMyBatis的取舍逻辑2.1 后端为什么是SpringBoot标题里明确了SpringBoot这本身也是Java Web开发的主流选择。选择它的理由不是大家都在用而是它解决了我最关心的两个问题一个是依赖配置成本。如果自己搭Spring MVC项目要手动配置DispatcherServlet、视图解析器、事务管理器还要管理一堆版本兼容问题。SpringBoot通过starter机制把常用场景的依赖打包好约定优于配置一个启动类加几个注解就能把工程跑起来开发效率高很多。另一个是内嵌容器。SpringBoot内嵌Tomcat打包成JAR就能直接运行不用在服务器上单独装Tomcat再扔WAR包对部署演示和本地测试都很友好。我选用的版本是SpringBoot 2.7.x。可能有人问为什么不上3.x主要考虑是生态兼容性2.7是2.x里的成熟版本大量教程和第三方启动器都以它为准踩坑时更容易找到解决方案。实际开发中稳定优先比版本最新更重要。2.2 持久层MyBatis为什么比JPA更适合这个项目标题指定了MyBatis我做技术对比后也确认它适合这个场景。这个系统的查询条件很多变土地列表要根据价格区间、认养状态、关键字组合筛选订单列表要根据时间范围、用户、状态做条件查询后台还要出统计报表。这些SQL用MyBatis的XML动态SQL写起来最直观比如if标签能根据传入条件动态拼接WHERE子句几乎可以做到接口一层不变SQL按需变化。而如果用JPA或者Spring Data JPA简单的增删改查确实省事但一旦查询条件复杂要么写JPQL要么用Specification构造复杂条件调试起来不如直接看SQL直观。MyBatis则把SQL完全暴露在你面前执行计划、索引命中情况都可以直接分析出了问题能快速定位。MyBatis还有一个好处是SQL和Java代码分离SQL写在XML里改动查询逻辑不需要重新编译Java代码。对于需要频繁调整报表查询的信息管理系统来说这个特性在后期维护阶段特别省心。2.3 前端Vue3 Element Plus Pinia 的组合前端技术栈我选了Vue3 Vite Element Plus Pinia axios。标题里只写了Vue实际上Vue3已经是很成熟的主流版本组合式API在写业务逻辑时比Vue2的Options API更灵活。Vite作为构建工具开发服务器启动快热更新体验好尤其在频繁调接口改页面时能明显感觉到效率提升。Element Plus提供现成的表格、表单、日期选择器、弹窗组件做管理端页面时不需要自己从零写组件能把精力集中在业务逻辑上。Pinia是Vue3官方推荐的状态管理库比Vuex轻量主要用来存用户登录信息和一些跨页面共享的状态。2.4 鉴权设计JWT拦截器就够用有的教程喜欢引入Spring Security但对这个项目来说它显然太重了。系统只有两种角色普通用户和管理员没有复杂的权限层级我用JWT配合Spring拦截器就能覆盖全部需求。流程是用户登录成功后后端签发一个包含userId和role的JWT令牌返回给前端前端在axios请求拦截器里把令牌放到请求头后端写一个拦截器拦截需要登录的接口路径校验令牌合法性和角色权限。整个实现只有几十行代码比Spring Security的过滤器链配置直观得多也容易讲清楚原理。3. 数据库建模土地档案、认养订单和预约排期怎么设计字段3.1 核心表单拆解数据库设计是这套系统的地基。我没有采用单表堆字段的方式而是围绕业务对象拆成了十几张表核心表大概这样user用户表id、username、password、real_name、phone、role、avatar、create_timeland土地档案表id、land_no编号、name、area、price_per_year、status可认养/已认养/维护中、cover_img、description、sort、create_timeproduct农产品表id、name、price、stock、unit、status、cover_img、descriptionorder认养订单表id、order_no、user_id、land_id、duration_months、total_amount、status、pay_time、cancel_time、expire_timeappointment预约记录表id、appointment_no、user_id、slot_id、appointment_date、time_slot、person_count、status、create_timeappointment_slot预约排期表id、slot_date、time_slot、max_count、booked_count、statusarticle资讯表id、title、content、cover_img、publish_status、publish_time、create_timenotice公告表id、title、content、status、create_time这里最需要注意的是两个逻辑关联设计。第一个是订单表里的快照字段。下单时我把土地名称、认养价格冗余存到订单表里而不是只存一个land_id。为什么这么做因为土地价格和名称可能调整如果不做快照半年后查历史订单时看到的价格可能是现在的价格数据真实性就没了。本质上是以空间换历史准确性。第二个是预约排期表。它单独维护一个时段可以被预约的最大人数和已预约人数而不是把每次预约都直接插入一条记录就完事。预约请求进来时后端在同一事务里校验booked_count max_count通过则插入预约记录并加一。这个表就是采摘场地的库存表。3.2 状态流转设计状态字段如果只是随手写一个字符串后面一定会乱。我在编码前先把状态机画清楚不画图表用文字描述认养订单状态流转WAIT_CONFIRM待确认用户提交订单后默认状态等待管理员确认收款。CONFIRMED认养生效中管理员确认收款后进入生效状态开始计算认养周期。CANCELED已取消用户或管理员在生效前取消。EXPIRED已到期到达订单expire_time后触发生效期内完结。预约记录状态流转BOOKED已预约用户成功提交预约。CANCELED已取消用户在预约日期前取消。COMPLETED已完成预约日期已过管理员标记完成或到达日期自动视为完成。状态设计最关键的原则是状态转变必须有明确触发动作。比如待确认到生效中触发动作是管理员点击确认收款已预约到已完成触发动作是系统时间越过预约日期。不要在代码里随意修改状态值所有变更都收敛到Service层的方法里。3.3 容易被忽略的字段细节有几个字段设计上的细节值得单独说金额字段一律用decimal(10,2)Java侧用BigDecimal。认养费用涉及年费、时长折算、可能的退款计算用double会有精度问题虽然看起来只是几分钱的误差财务对不上账时就麻烦了。时间字段统一用datetimeJava侧用LocalDateTime不用Timestamp。后面会单独讲为什么前端显示时间会多出一个字母T这也是一个必踩的坑。所有业务表都加上deleted逻辑删除字段tinyint默认0不采用物理删除。用户误删了一条预约记录物理删除后就彻底查不回来逻辑删除至少能在管理端做回收站级别的补救。订单号、预约单号由后端生成格式类似ORD 时间戳 随机数而不是直接用自增主键当业务编号。自增主键暴露出去容易被遍历而且业务沟通里说看一下订单5803远不如看一下ORD20250112xxxx直观。4. 后端实现细节事务、动态SQL和时间序列化的处理4.1 分层结构与调用链后端代码我保持经典的四层结构Controller接收请求→ Service业务逻辑→ Mapper数据库操作— Model实体/DTO/VO。没有为了炫技引入复杂的设计模式但这四层每一层都承担明确的职责避免把SQL拼在Controller里。以创建认养订单为例调用链是这样的用户在前端点击立即认养前端请求POST /api/order/create。Controller层拿到DTO后先做参数校验土地ID是否为空、认养时长是否合法。Service层开始业务校验土地是否存在、状态是否为可认养、用户是否已经认养过同一块地。校验通过后生成订单号计算总金额插入订单记录(statusWAIT_CONFIRM)。在同一个事务里把土地状态改为已认养防止其他人重复下单。返回订单信息给前端前端跳转到待确认页面。这段逻辑里业务校验和数据库操作必须在一个事务里完成如果订单插进去了但土地状态更新失败需要整体回滚。用Transactional注解标记在Service方法上Spring会把事务边界维护好这比在Mapper层手动写commit/rollback可靠得多。4.2 MyBatis动态SQL在多条件筛选中的写法后台土地管理页面有一个常见的筛选需求管理员输入关键字选择认养状态再设置一个价格范围点查询后看到符合条件的土地列表。用MyBatis的XML映射文件写这个查询非常顺手select idselectLandList resultTypecom.example.entity.Land SELECT * FROM land where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR land_no LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null and status ! AND status #{status} /if if testminPrice ! null AND price_per_year gt; #{minPrice} /if if testmaxPrice ! null AND price_per_year lt; #{maxPrice} /if AND deleted 0 /where ORDER BY sort ASC, create_time DESC /selectwhere标签会自动去掉第一个多余的ANDif标签根据传入参数决定是否拼接条件这样一套代码就能应对多种查询组合不需要为每个条件组合单独写一个方法。需要注意gt;和lt;是XML里大于号小于号的转义写法直接写在某些XML解析器下会报错这是新手很容易栽跟头的地方。4.3 事务边界为什么下单必须加Transactional拿预约采摘举例。一个完整预约动作涉及查询排期时段判断booked_count是否小于max_count。插入一条预约记录。更新排期时段booked_count加一。三步操作任何一个失败都不能留下半完成的数据——如果插入预约成功但计数没更新后面就会超卖。Transactional在这里把三步操作绑定为一个原子操作要么全部成功要么全部回滚。具体实现时用MySQL的SELECT ... FOR UPDATE配合事务做行级锁确保高并发下不会出现两个请求同时读到booked_count 4而max_count 5导致最后实际预约6人的问题。单机部署时这种方案简单可靠不需要引入分布式锁。4.4 统一返回体和全局异常处理前后端分离项目里接口返回格式统一能省掉大量联调沟通过程。我的后端所有接口都返回Result对象结构固定为{ code: 200, message: 操作成功, data: {} }code为200表示成功非200表示业务失败或系统异常。前端axios响应拦截器里只判断code不用每次根据HTTP状态码猜后端发生了什么。配套的还有全局异常处理器用RestControllerAdvice捕获两类异常业务异常返回code400和对应的友好提示系统异常记录日志后返回code500和系统繁忙提示。这样前端拿到错误后直接提示message不会把一堆堆栈信息抛给用户。5. Vue前端实现从登录到认养下单的完整页面链路5.1 项目初始化与目录组织前端用Vite创建项目后我按页面功能而不是技术类型来组织目录src/ api/ // 按模块划分的接口请求函数 auth.js land.js order.js appointment.js views/ // 页面组件 user/ LandList.vue LandDetail.vue OrderCreate.vue OrderList.vue AppointmentCreate.vue admin/ Dashboard.vue LandManage.vue OrderManage.vue SlotManage.vue components/ // 复用型组件 StatusTag.vue Pagination.vue router/ index.js store/ user.js utils/ request.js这样做的目的是让每个模块的内聚度高API请求函数和对应页面同名看到页面名基本能猜到接口文件名。项目规模到这种程度不需要复杂的文件夹分层层层套反而平铺更高效。5.2 axios封装和路由权限控制axios封装是前端的核心基础设施。我在request.js里统一做了三件事请求拦截器从localStorage取出token放到请求头Authorization字段。响应拦截器统一解析Result结构code不为200时弹出错误提示。401处理后端识别到token无效时前端自动跳转登录页并清空本地登录态。路由权限用router.beforeEach实现router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else if (token to.meta.roles !to.meta.roles.includes(store.user.role)) { next(/403) } else { next() } })这样用户端页面要求登录管理端页面还额外要求role为admin前端拦截只是体验上的便利真正的权限校验在后端拦截器里把关。5.3 认养流程的组件化实现认养流程是最长的前端链路土地列表→详情页→选择认养时长→提交订单→订单状态页。这个流程拆成了几个页面组件但共享同一个useLandOrder组合式函数管理提交状态。以提交订单页面为例核心逻辑是从路由参数读取landId详情接口拉取土地信息和年费。用户选择认养时长6/12/24个月前端即时计算总金额。点击提交后调用创建订单接口成功后跳转到订单列表页并提示等待管理员确认。这里有一个容易忽略的产品细节土地状态为维护中时前端不能只把按钮置灰后端创建订单接口也要再次校验状态。前端禁用了接口照样可以被恶意调用双端校验才是安全的做法。5.4 管理端表格页面的增删改查套路管理端页面基本是同一个套路顶部筛选区 表格区 分页 右上角新增/编辑按钮 弹窗表单。以土地管理页为例筛选区关键字输入框、状态下拉框、价格范围输入框点击查询触发列表接口重查。表格列土地编号、名称、面积、年费、状态用StatusTag组件渲染成不同颜色的标签、操作列。操作列包含编辑上架/下架删除不同状态显示不同的操作按钮避免无意义操作。弹窗表单用的是Element Plus的el-dialog嵌套el-form提交前做表单校验必填项、金额必须为正数成功后刷新列表并关闭弹窗。这套筛选表格弹窗的模式可以复用到订单管理、排期管理、资讯管理页面上所以我把表格封装了一个通用组件传入列配置和数据请求方法就能渲染页面几百行代码的事变成几十行。6. 前后端联调与部署阶段我踩过的4个坑6.1 跨域配置为什么前端拿不到响应本地联调时前端跑在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); } }用allowedOriginPatterns而不是allowedOrigins是因为后者在配合allowCredentials(true)时不允许使用通配符而前者支持。这个坑的报错信息是Value of the Access-Control-Allow-Origin header in the response must not be the wildcard *网上答案很多但自己绕一圈才明白是版本API差异。6.2 LocalDateTime序列化时间莫名多了一个T这是我联调时遇到最频繁的问题。后端返回LocalDateTime类型的字段默认序列化出来是2025-01-12T10:30:00这样的格式中间多了一个字母T前端怎么格式化都别扭。解决方案是在配置文件里统一指定格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时需要在实体类的LocalDateTime字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。两个地方都配置好前端拿到的才是2025-01-12 10:30:00这种标准格式。至于为什么必须有注解因为spring.jackson.date-format对java.util.Date生效但LocalDateTime的序列化器不会自动读到这个全局配置这是Jackson版本行为的一个经典差异。6.3 MySQL连接时区和SSL警告启动项目时控制台一直刷警告WARN: Establishing SSL connection without servers identity verification is not recommended.虽然不影响运行但看着很烦。再往后用LocalDateTime从数据库查询时间时会发现时间对不上因为MySQL连接时区没有指定。我用的连接配置是spring: datasource: url: jdbc:mysql://localhost:3306/leyang?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai这里有个细节容易被忽略MySQL服务器本身的时区是系统默认时区如果服务器是UTC即使加了serverTimezoneAsia/Shanghai也可能出现你存入的是14点、查出来变成22点的情况。排查方式是在数据库执行SELECT NOW();看返回的系统时间和当前时间是否一致不一致就去检查MySQL的default-time-zone配置。6.4 刷新页面404路由模式的取舍项目做完准备部署演示时发现一个尴尬问题用户访问首页正常但在某个页面按F5刷新就直接404。这是Vue Router的history模式和服务器配置不匹配导致的。history模式依赖服务器把所有非文件请求都转发到index.html请求的是/order/list这个路径服务器没有对应的静态资源就返回404。解决方式有两种直接用createWebHashHistoryURL变成/#/order/list的形式任何刷新路径都能正确映射到首页再走前端路由配置零成本。也可以保留createWebHistory然后在Nginx里增加try_files $uri $uri/ /index.html;配置。对我这个演示场景hash模式显然更省事我最终选了它。但如果项目要上线并做SEOhistory模式才是正选两种模式的取舍要结合部署环境来定。7. 验收清单与后续扩展建议7.1 功能验收怎么测项目跑起来之后我整理了一份验收清单逐项验证而不是看一眼觉得能用就行。下面是核心用例表功能模块操作步骤预期结果用户注册登录新用户注册退出后重新登录注册成功登录后跳转首页令牌写入本地存储土地列表筛选关键字状态价格区间组合查询列表返回符合条件的数据分页正常创建认养订单对可认养土地提交认养一年订单状态为待确认土地状态变为已认养重复下单拦截对同一块已认养土地再次下单后端返回该土地已被认养的业务提示预约采摘选择一个可预约时段提交预约预约记录生成该时段已预约人数加1管理端审核订单管理员确认一笔待确认订单收款订单状态变为生效中生效时间写入越权访问普通用户访问管理端接口后端返回无权限提示前端跳转403页面验收时我额外建议准备一个并发下单小测试用两个浏览器窗口同时提交同一块地的认养订单正常情况下只会成功一个。如果你的代码里没有加事务和锁这一步会直接暴露出重复下单的问题这个测试在演示时讲出来非常加分。7.2 让项目更完整的扩展方向这套系统跑通后我列出了几个值得继续做的方向按性价比排序接入ECharts做管理端经营可视化把认养订单量、预约趋势、土地利用率画成图表管理端价值会明显上一个台阶。增加文件上传能力土地图片和资讯封面从本地上传改为对象存储生产环境不能把所有图片都放在应用目录下。把操作日志表补上谁在什么时间审核了哪笔订单都记录在案这套系统才有真正的审计能力。微信小程序端复用同一套后端接口前端重新开发一套即可用户端的触达效果会好很多。回到标题本身这套基于SpringBootVue的系统代码层面并不复杂复杂的是把认养预约的场景用数据模型完整表达出来。我在实际开发中最大的体会是花在数据库设计和状态流转上的时间最终都会在前后端联调阶段省回来。如果你打算照着这个思路自己做一遍我建议不要直接复制谁的源码先拿着笔把订单状态、土地状态、排期表的关系画清楚再动手敲代码你会发现后面每一步都很顺。
阅读完成 · 觉得有帮助?