前后端分离的农业设备租赁系统,是我近期完整落地的一个真实项目。项目用SpringBoot做后端接口、Vue做前端页面、MyBatis操作MySQL数据库,整套源码加部署流程我都整理了出来。这篇博文就从业务设计、核心表结构、接口开发、前端页面到服务器部署,一步一步说清楚,包括那些官方文档里不会写的坑。
如果你正在做毕业设计、接私活,或者想系统走一遍前后端分离项目的完整流程,这篇文章应该能帮你少走不少弯路。
1. 项目整体设计与系统定位
1.1 农业设备租赁的业务本质
农业设备租赁和普通商品租赁有本质区别。普通商品(比如图书、衣服)是标准化物件,库存管理简单,但农业设备(拖拉机、收割机、无人机)有几个特殊属性:
- 设备价值高:一台收割机动辄几十万,租赁期间的安全和保养问题必须纳入系统管理
- 时间窗口强:农忙季节就那么几周,设备空闲一天都是损失,租赁日历和排期是核心
- 损耗不可预测:同一台设备,不同的作业场景损耗差别极大,押金和赔付规则必须灵活
- 线下操作多:看设备、提货、验货、归还这些环节,系统要做到线上预约、线下执行,数据要闭环
所以我做这个系统时,没有简单照搬“商品-订单-支付”这种通用电商模型,而是把农业租赁特有的业务规则融入了字段设计、状态机流转和权限边界里。
整个系统拆成三个端:用户端(农户/租用方)、管理端(设备运营方)、后台管理(管理员视角)。前后端分离后,三个端共用同一套后端API,前端通过Vue Router做路由级别权限控制,后端通过Spring Security + JWT做接口级鉴权。
1.2 为什么选SpringBoot+Vue+MyBatis+MySQL这套组合
这套技术栈在这个场景下不是最炫的,但一定是最稳的。
- SpringBoot:Java后端的绝对主流,生态成熟,招人好招,教程和问题方案最丰富。用Spring Initializr生成项目骨架,依赖管理交给Maven,十分钟就能跑起来一个Web服务。相比SSH老架构,省掉了大量XML配置,开发效率提升明显
- MyBatis:很多人纠结用MyBatis还是JPA,农业租赁这种业务有个特点——查询条件复杂、多表关联多、SQL优化空间大。MyBatis的XML里写SQL,动态SQL处理复杂条件(设备筛选、订单列表)比JPA直白得多。而且MyBatis对DBA友好,SQL调整不需要改Java代码
- Vue:前端用Vue 2 + Element UI。Vue的响应式数据绑定,在做表单校验、动态加载订单状态、设备实时库存这些交互场景时,开发效率很高。Element UI提供现成的表格、表单、弹窗、日期选择器,做完管理后台的速度非常快
- MySQL:数据量撑到百万级没问题,InnoDB引擎对事务的支持可靠,租金计算、订单状态流转这些场景必须依赖事务保证数据一致性
提示:当时没有引入Redis、RabbitMQ、Elasticsearch这些中间件,核心原因是项目体量不需要。加了中间件等于给部署和运维增加三倍复杂度,对于农业设备租赁这个场景,一台4核8G的服务器跑MySQL+后端+前端完全够用。架构不是越复杂越好,够用且稳定才是第一原则。
1.3 需求分析与功能模块划分
项目立项前,我花时间调研了线下农机租赁站的运营模式,最终把系统需求收敛成四大核心模块。
| 模块 | 功能点 | 核心逻辑 |
|---|---|---|
| 设备管理 | 设备录入、编辑、上下架 | 设备状态机:空闲→已预约→租赁中→维修中→已下架 |
| 租赁订单 | 创建订单、支付押金、续租、归还 | 订单状态机:待支付→待提货→租赁中→待归还→已完成/已取消 |
| 客户管理 | 注册、实名认证、信用记录 | 实名信息与订单关联,作为押金计算依据 |
| 系统管理 | 用户权限、操作日志、数据统计 | RBAC权限模型,运营数据看板 |
实际开发中,我强烈建议先把订单状态机画清楚再动手写代码。这个系统里最复杂的就是订单状态流转——押金未支付可以取消、设备已提货不能直接取消、租期到了有逾期逻辑、归还后有损坏鉴定流程。状态机设计不清楚,后面写接口会反复改表结构。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 核心表与字段设计
数据库是整套系统的地基,表设计得好不好,直接决定后期开发顺畅程度。一共设计了11张表,核心的6张我列在这里:
设备表(device)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| device_name | varchar(100) | 设备名称 |
| device_type | varchar(50) | 设备类型(拖拉机/收割机/播种机等) |
| daily_price | decimal(10,2) | 日租金 |
| deposit | decimal(10,2) | 押金金额 |
| status | tinyint | 状态:0空闲 1已预约 2租赁中 3维修中 4已下架 |
| image_url | varchar(255) | 设备图片 |
| location | varchar(200) | 设备所在地区 |
| description | text | 设备描述 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
这里特别注意:status字段不要用枚举写死在代码里,用tinyint存状态值,状态含义在Java枚举里定义。这样以后要加状态(比如“已报废”)只需要在枚举里加一个值,不用改数据库表结构。
租赁订单表(rent_order)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(64) | 订单编号 |
| user_id | bigint | 租用用户ID |
| device_id | bigint | 设备ID |
| start_time | datetime | 租赁开始时间 |
| end_time | datetime | 租赁结束时间 |
| total_amount | decimal(10,2) | 租金总额 |
| deposit_amount | decimal(10,2) | 押金金额 |
| status | tinyint | 订单状态 |
| pay_status | tinyint | 支付状态:0未支付 1已支付 2已退款 |
| real_start_time | datetime | 实际提货时间 |
| real_end_time | datetime | 实际归还时间 |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
订单编号用了“日期+随机数”的方案,格式为:yyyyMMddHHmmss + 6位随机数。不要直接用自增ID暴露给用户,这个订单号后面做对账和客服查询都方便。
用户表(user):保存用户名、密码(BCrypt加密存储)、手机号、身份证号、信用分、角色标识。
系统管理相关表:用户角色表、角色权限表、操作日志表。
2.2 防重复租赁的时间冲突处理
设备租赁最容易出的问题是相同时间段重复租赁。这个场景必须用SQL做一次数据库层面的冲突检测,而不是只靠应用层判断。
我的方案是:在创建订单时执行一次重叠时间查询:
sql复制SELECT COUNT(*) FROM rent_order
WHERE device_id = #{deviceId}
AND status IN (1, 2, 3) -- 待提货、租赁中、待归还
AND (start_time < #{newEndTime} AND end_time > #{newStartTime})
如果查询结果大于0,说明该设备在目标时间区间内已经被占用,直接拒绝创建订单。这个逻辑看着简单,但有个细节要处理好:订单状态是已取消或已完成的记录要排除掉,否则历史订单会挡住新的租赁,这个坑实际开发中很容易踩。
2.3 MyBatis动态SQL与多表关联设计
MyBatis的XML是处理复杂查询的主要战场。以“设备列表按条件筛选”为例,需要支持按设备类型、地区、价格区间、关键字搜索:
xml复制<select id="selectDeviceList" resultType="com.agri.entity.Device">
SELECT d.*, IFNULL(AVG(r.score), 5) AS avg_score
FROM device d
LEFT JOIN rent_order ro ON d.id = ro.device_id
LEFT JOIN review r ON ro.id = r.order_id
<where>
<if test="deviceType != null and deviceType != ''">
AND d.device_type = #{deviceType}
</if>
<if test="location != null and location != ''">
AND d.location LIKE CONCAT('%', #{location}, '%')
</if>
<if test="maxPrice != null">
AND d.daily_price <= #{maxPrice}
</if>
<if test="keyword != null and keyword != ''">
AND (d.device_name LIKE CONCAT('%', #{keyword}, '%')
OR d.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
GROUP BY d.id
ORDER BY d.create_time DESC
</select>
这里的动态SQL解决了“用户可能不筛选任何条件”的场景——如果每个条件都用字符串拼接,不仅容易出错而且会引发SQL注入风险。MyBatis的<where>标签自动处理多余的AND,代码干净也安全。
提醒一个容易忽略的问题:MySQL里
decimal(10,2)字段传给Java时请用BigDecimal接收,不要用Double。租金、押金这类涉及金额计算的数据,用浮点数会丢失精度,这种错误排查起来非常隐蔽。
3. 后端核心功能实现
3.1 SpringBoot项目结构与接口规划
后端项目结构用的是标准的三层架构(Controller → Service → Mapper),这个结构在中小型项目中清晰度和可维护性最好:
code复制com.agri.rental
├── controller # RESTful接口层
├── service # 业务逻辑层(接口+实现类)
├── mapper # MyBatis数据访问层
├── entity # 数据库实体类
├── dto # 数据传输对象(参数接收、VO返回)
├── config # Spring配置类(安全、跨域、拦截器)
├── common # 通用类(结果封装、异常处理、工具类)
└── security # 登录认证与权限控制
接口设计遵循RESTful风格,核心接口清单如下:
| 方法 | 路径 | 说明 | 权限 |
|---|---|---|---|
| GET | /api/device/list | 设备分页列表 | 公开 |
| GET | /api/device/ | 设备详情 | 公开 |
| POST | /api/order/create | 创建租赁订单 | 登录用户 |
| POST | /api/order/pay | 订单支付 | 登录用户 |
| POST | /api/order/pickup | 确认提货 | 管理员 |
| POST | /api/order/return | 确认归还 | 管理员 |
| GET | /api/order/my | 我的订单列表 | 登录用户 |
3.2 订单状态机的完整流转
订单状态是这套系统的核心业务逻辑。开发前我把状态流转图画了出来(不用代码画,就是一张纸),然后在Java里用枚举+行为方法控制状态的迁移,不允许随意修改:
java复制public enum OrderStatus {
PENDING_PAY(0, "待支付"),
PENDING_PICKUP(1, "待提货"),
RENTING(2, "租赁中"),
PENDING_RETURN(3, "待归还"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消"),
OVERDUE(6, "已逾期"),
DISPUTED(7, "争议中");
private final int code;
private final String desc;
// getter、constructor
}
状态流转的约束必须放在Service层,通过状态判断方法保证合法流转:
java复制public void returnDevice(OrderReturnDTO dto) {
RentOrder order = orderMapper.selectById(dto.getOrderId());
if (order == null) {
throw new BusinessException("订单不存在");
}
// 只允许状态为“租赁中”或“待归还”的订单执行归还
if (order.getStatus() != OrderStatus.RENTING.getCode()
&& order.getStatus() != OrderStatus.PENDING_RETURN.getCode()) {
throw new BusinessException("当前订单状态不支持归还操作");
}
// 归还业务逻辑...
order.setStatus(OrderStatus.COMPLETED.getCode());
orderMapper.updateById(order);
}
这样做的好处是杜绝了“跳过前置流程直接修改状态”的安全漏洞。之前见过有些系统直接把状态字段暴露给前端,用户改个请求参数就能绕过流程,这是严重的逻辑缺陷。
3.3 JWT认证与权限拦截
前后端分离项目的最大痛点就是登录状态管理。不能用传统的Session方案(跨域会出问题),我选择用JWT做无状态认证。
核心配置流程:
- 登录接口:用户提交用户名密码,后端校验通过后生成JWT令牌返回给前端
- 前端存储:前端把Token存在
localStorage中,每次请求在Header携带Authorization: Bearer <token> - 后端校验:Spring Security过滤器拦截请求,解析JWT并设置登录用户上下文
- 权限控制:通过自定义注解
@PreAuthorize("hasRole('ADMIN')")控制管理接口的访问
JWT工具类的关键方法:
java复制public String generateToken(Long userId, String role) {
long now = System.currentTimeMillis();
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("role", role)
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + EXPIRE_TIME))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
注意一个关键点:JWT密钥SECRET_KEY必须放在
application.yml配置文件中,不要写死在代码里。而且生产环境的密钥要足够长(至少32位),否则有被暴力破解的风险。
3.4 跨域配置与统一异常处理
前端跑在http://localhost:8080,后端跑在http://localhost:9090,开发阶段必然有跨域问题。SpringBoot的跨域配置写在单独配置类里:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
这里有一个坑:如果使用了Spring Security,跨域配置不能只在WebMvcConfigurer里配,必须在SecurityConfig里也允许OPTIONS请求,否则安全过滤器会在CORS处理之前把预检请求拦截掉,前端会一直报CORS错误。
统一异常处理也是后端开发必须做的一件事情。我在GlobalExceptionHandler里用@RestControllerAdvice捕获业务异常、参数校验异常和未知异常,统一返回结构化结果:
json复制{
"code": 500,
"message": "设备已被预约,请选择其他时间",
"data": null
}
前端只要判断code === 200就正常处理数据,其他情况一律弹错误提示,这样前后端联调省了很多时间。
4. 前端项目搭建与核心页面实现
4.1 Vue环境搭建与工程结构
前端使用Vue 2 + Vue Router + Vuex + Element UI + Axios这套成熟组合。环境搭建这里有几个容易出问题的点:
Node.js版本选择:Vue 2项目建议使用Node.js 14.x或16.x版本。Node 17以上版本在编译时容易报error:0308010C:digital envelope routines::unsupported错误,这是OpenSSL版本不兼容导致的。如果已经装了高版本Node,可以临时设置NODE_OPTIONS=--openssl-legacy-provider绕过,但推荐直接安装Node 14,一劳永逸。
初始化项目:
bash复制# 安装Vue CLI
npm install -g @vue/cli@4.5.15
# 创建项目(选择Vue 2版本)
vue create agri-rental-frontend
# 安装核心依赖
npm install element-ui axios vuex vue-router
4.2 前端路由与权限控制
前端路由分两层设计:第一层是公共路由(登录页、注册页、设备列表页),第二层是受保护路由(个人中心、订单管理、后台管理)。Vue Router的beforeEach路由守卫做登录拦截:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next()
}
})
对于管理员页面,在路由meta里标记requiresAdmin: true,然后在路由守卫里做二次校验,同时判断用户角色是否为管理员。前端做权限控制的主要目的是提升用户体验,真正的数据安全必须依赖后端接口权限控制,这个主次关系要清晰。
4.3 Axios请求封装与拦截器
前端与后端交互的统一入口是Axios拦截器,我把它单独封装在utils/request.js里:
javascript复制import axios from 'axios'
import { Message } from 'element-ui'
import router from '../router'
const service = axios.create({
baseURL: '/api',
timeout: 15000
})
// 请求拦截器:自动携带Token
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
// 响应拦截器:统一处理业务码和登录失效
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === 200) {
return res
} else if (res.code === 401) {
Message.error('登录已过期,请重新登录')
localStorage.removeItem('token')
router.push('/login')
return Promise.reject(new Error('Unauthorized'))
} else {
Message.error(res.message || '操作失败')
return Promise.reject(new Error(res.message))
}
},
error => {
Message.error('网络异常,请稍后重试')
return Promise.reject(error)
}
)
export default service
4.4 核心页面实现:设备列表与订单流程
设备列表页是用户接触系统的入口页面,交互设计直接影响到租用转化率。页面顶部是筛选条件区(设备类型、地区、价格范围),主体是设备卡片列表,每个卡片展示设备图、名称、类型标签、日租金。点击“立即租用”会弹出日期选择对话框,选择开始和结束时间后自动计算租金总额并展示押金金额,用户确认后提交订单。
日期选择组件这里有个细节:选择的开始日期不能早于今天,结束日期不能早于开始日期。Element UI的DatePicker组件支持picker-options属性设置禁用日期:
javascript复制pickerOptions: {
disabledDate(time) {
return time.getTime() < Date.now() - 8.64e7 // 禁止选择今天之前
}
}
订单管理页处理的是用户所有订单的展示和操作。每个订单卡片按状态分类展示,待支付的订单有“去支付”按钮,租赁中的订单显示剩余天数。后端返回的订单数据里有statusCode,前端根据状态码映射成对应的文字和颜色,用标签组件展示。
5. 系统部署与上线流程
5.1 打包与本地预演
部署前,先在本地跑一遍完整的打包流程,确认无报错再上服务器。
后端打包:
xml复制<!-- pom.xml 中配置打包插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
执行mvn clean package,生成的可执行jar包在target/目录下。启动命令:
bash复制java -jar agri-rental-backend.jar --spring.profiles.active=prod
后端配置分离的好处是:开发环境用application-dev.yml(本地数据库、开启SQL日志),生产环境用application-prod.yml(线上数据库、关闭日志、减小日志级别)。
前端打包:
bash复制npm run build
打包产物在dist/目录下,里面是纯静态文件(HTML、CSS、JS),需要由Nginx提供服务。
5.2 Nginx部署配置
服务器环境是CentOS 7.9 + Nginx 1.20 + MySQL 8.0 + JDK 1.8。Nginx承担两个职责:一是托管前端静态文件,二是反向代理后端接口。
nginx复制server {
listen 80;
server_name your-domain.com;
# 前端静态文件
root /usr/share/nginx/html;
index index.html;
# 前端路由history模式配置:所有请求都指向index.html
location / {
try_files $uri $uri/ /index.html;
}
# 后端API反向代理
location /api/ {
proxy_pass http://127.0.0.1:9090;
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 /uploads/ {
alias /data/app/uploads/;
}
}
这里Vue的history路由模式(去掉URL中的#号)必须配合Nginx的
try_files配置,否则用户刷新/order/list页面会报404。如果不想配置Nginx,可以改用hash模式(URL中有#号),刷新就不会出问题,但URL不够美观。
前端项目的axios请求路径在开发环境配置了Vue CLI代理转发到后端9090端口,生产环境的baseURL建议直接配置为相对路径/api,由Nginx做同域转发,避免跨域。生产环境所有请求都走80端口,浏览器不感知后端的真实地址,运维上也更安全。
5.3 MySQL初始化与数据库迁移
数据库初始化有两种方式,我推荐项目上线时用source命令执行SQL脚本:
bash复制mysql -u root -p < agri_rental.sql
生产环境数据库配置的几个要点:
- 创建专用数据库账号,不要用root账号连接应用,权限只需
SELECT, INSERT, UPDATE, DELETE即可 - 字符集统一使用
utf8mb4,排序规则使用utf8mb4_general_ci,避免中文乱码问题 - 连接串增加参数
useSSL=false&serverTimezone=Asia/Shanghai,避免SSL握手超时和时区偏差
应用启动后,登录后台确认设备分类正常显示、创建测试订单走完整流程,所有页面打开无报错,部署即算完成。
6. 常见问题与排查技巧实录
6.1 SpringBoot版本引发的编译异常
这次开发开始时,Spring Initializr默认拉取的是SpringBoot 3.x版本。3.x版本相比2.x有一些关键变化,其中影响最大的是javax.包名迁移到了jakarta.。如果项目里还在使用javax.servlet等旧包名,编译直接报错。
我的建议是:这个项目使用SpringBoot 2.7.x版本,原因是2.7.x是2.x系列的最终维护版本,稳定可靠,且网上查得到的资料和方案最多,配合JDK 1.8没有兼容性风险。SpringBoot 3.x必须配合JDK 17以上使用,对服务器环境要求更高。
6.2 MyBatis配置了Mapper却一直报“Invalid bound statement”
这是MyBatis最常见的集成问题。SpringBoot项目中Mapper接口和XML文件不在同一个目录,或者没有正确配置扫描路径,就会报这个错误。
我的排查步骤:
- 检查
application.yml中的mybatis.mapper-locations配置是否正确,应该指向classpath下的mapper目录:yaml复制mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.agri.rental.entity configuration: map-underscore-to-camel-case: true - 确认XML文件的namespace是否与Mapper接口全限定名一致
- 确认Mapper接口上的
@Mapper注解是否添加,或者启动类上使用@MapperScan注解 - 确认target目录中是否已经生成对应的XML文件,如果没生成,可能是IDE没编译资源文件,Maven重新打包即可
6.3 前端npm install一直卡住或下载失败
在国内开发,npm依赖下载慢是绕不开的问题。项目初始化时将npm源切换到淘宝镜像:
bash复制npm config set registry https://registry.npmmirror.com
如果个别包仍然下载失败,先删除node_modules目录和package-lock.json文件后重新npm install。不要盲目升级包版本,Vue 2项目升级Element UI到2.15+版本以上时,有些组件样式和API已经变了,原项目代码可能不兼容。
6.4 订单时间重叠和脏数据问题
上线试运行阶段遇到过一个问题:两条并发请求同时创建同一设备同一时段的订单,数据库层的时间冲突检测没有拦下来。原因是MySQL默认的隔离级别是REPEATABLE READ,两个事务并发查询时都认为设备空闲,然后一起插入成功。
解决方案是在rent_order表的(device_id, start_time, end_time)上增加一个索引,同时利用MySQL的唯一约束来兜底。更好的方案是给设备表加一个lock_version乐观锁字段,更新设备状态时带上版本号,更新失败则说明设备被并发修改,重新查询判断:
java复制int rows = deviceMapper.updateStatusWithVersion(deviceId, oldStatus, newStatus, version);
if (rows == 0) {
throw new BusinessException("设备状态已变更,请刷新后重试");
}
这种方案既解决了并发问题,对系统性能也没有影响。
6.5 常用排查命令速查表
| 问题现象 | 排查命令 | 排查要点 |
|---|---|---|
| 后端端口被占用 | ss -tlnp | grep 9090 |
确认9090端口未被其他进程占用 |
| 服务启动失败 | journalctl -u agri-rental -n 200 |
查看systemd服务最近200行日志 |
| MySQL连接失败 | mysql -u username -p -h 127.0.0.1 |
确认账号密码、权限、网络策略 |
| 前端白屏无报错 | 浏览器F12打开Console | 查看JS报错,重点看404资源文件 |
| API请求超时 | curl -v http://127.0.0.1:9090/api/device/list |
先本地直连后端,确认后端是否正常 |
7. 项目扩展方向与实际经验总结
项目完成上线后,有几块后续可以考虑扩展的方向,开发架构上已经预留了相应的基础设计。
信用分体系:在用户表已经设计了一个credit_score字段,后续可以增加租前信用审核、逾期扣分、按时归还加分、设备损坏扣分等逻辑。信用分高的用户可以直接降低押金金额,提升用户体验。
消息通知:当前所有提醒都需要用户登录系统查看,不够实时。后续可以接入短信通知(在订单状态变化时发送提醒,比如提货成功、距离归还日期不足一天、逾期未归还等场景)。接短信服务时把发送逻辑做成独立的Service接口,后端调用时不影响主流程。
移动端适配:现在前端是PC优先的Web页面,真实使用场景中,农户更习惯用手机浏览设备、下订单。后续可以考虑用Vue 3 + Vant重做一套移动端界面,后端接口完全复用,只需新增移动端前端工程。
最后分享两个这次开发中体会最深的经验。
第一,设计阶段多花时间画图,比写代码省十倍时间。设备状态机、订单状态机、权限模型这三张图,在编码前全部画清楚,后面几乎没改过核心表结构。反观之前跳过设计直接建表的项目,开发中期反复改字段、加状态,代码越写越乱。
第二,环境一致性是多人协作的隐形杀手。这次专门写了一份README.md放在项目根目录,明确记录了JDK版本(1.8)、MySQL版本(8.0)、Node版本(14.x)、Maven版本(3.6+)以及所有中间件的启动方式。新成员加入后照着文档操作,半小时内就能把环境跑通,省去了大量沟通成本。
这个项目从零到上线,整个流程走下来,核心收获不是用通了哪个框架,而是明白了前后端分离项目的完整闭环:需求分析要落到业务场景,表结构设计要匹配业务流程,接口设计要服务多方前端,部署上线要考虑实际运维。这套思路放到任何项目中都是通用的。
