最近在整理一套 Java Web 网上租赁系统的交付源码,技术栈正好是标题里这套:SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0。从项目初始化到把文档、SQL脚本、前后端工程打包成一份“拿到就能跑、跑完能看懂”的源码,中间踩了不少坑,也整理出了一套比较顺手的开发路径。这套内容适合两类人:一类是在做课程设计或毕业设计,需要一个完整业务闭环参考的 Java Web 项目;另一类是刚从前端分离的旧项目迁移过来,想看看 SpringBoot2 和 Vue3 真实协作时怎么处理接口、权限、分页、联调这些细节的同学。这篇就以网上租赁系统为例,把技术选型的底层逻辑、后端核心实现、前端组件化、MySQL8.0 运行参数,以及交付前需要检查的事项一次性说清楚。
1. 网上租赁系统这套技术栈,为什么我敢直接拿来交付
1.1 SpringBoot2:不是最新,但是最稳
很多同学一看到 SpringBoot2 就觉得“过时了”,毕竟 Spring Boot 3 和 Spring Framework 6 已经发布有一段时间了。但在实际交付项目里,SpringBoot2 仍然是最保守、最稳妥的选择,原因很直接:JDK8 的生态太成熟了,大量服务器、云主机、培训机构环境、学校机房默认就是 JDK8,而 SpringBoot3 强制要求 JDK17。如果目标用户是不想折腾环境的同学,或者部署环境比较老旧,SpringBoot2 + JDK8 几乎是零成本启动。
更重要的是,SpringBoot2 对应的一整套依赖版本都是经过大量生产环境验证的,比如 Spring MVC 5、Tomcat 9、MyBatis-Spring 2.x,网上随便搜都能找到对应的踩坑记录和解决方案。反观升级到 SpringBoot3 之后,javax 命名空间全部换成了 jakarta,很多老项目代码直接编译不过,MyBatis-Plus 也需要升级到专门适配的版本。为了一个业务系统去承担这种迁移成本,完全不值得。
所以我最终选型是 SpringBoot 2.7.x,这是 SpringBoot2 系列的最后一个维护版本,既保留了 SpringBoot2 的兼容性,又修掉了早期版本的大量 bug,还能配合 MyBatis-Plus 3.5.x 使用,属于这个组合里最舒服的版本区间。
1.2 Vue3:组合式API带来的实际收益
Vue2 到 Vue3 表面上是响应式系统的重构,实际开发里感受最深的其实是组合式 API(Composition API)带来的代码组织方式变化。以租赁系统的后台管理页面为例,物品管理、订单管理、用户管理这三个页面有大量相似逻辑:加载列表、分页查询、搜索、弹窗表单、删除确认。用 Vue2 的 Options API 写,每个页面都要把 data、methods、watch 分开,改一个功能要上下跳着改;换成 Vue3 的 <script setup> 之后,逻辑可以按照“这个页面要做什么”去组织,比如把分页相关变量和方法聚在一起,把表单校验逻辑聚在一起,代码可读性高很多。
我实际开发时还会配合组合式函数(Composable)把可复用逻辑抽出来,比如 usePagination 管理分页状态,useFormDialog 管理弹窗的开关和提交状态。这个模式在租赁系统里效果很明显,三个管理页面的重复代码至少减少了一半。
Vue3 对 TypeScript 的支持也比 Vue2 好了不止一个层级。Vue2 里用 TS 写组件经常要各种 Vue.extend 或者到处写类型断言,Vue3 配合 <script setup lang="ts"> 之后,props 和 emit 的类型检查非常自然。虽然这个项目我为了降低上手门槛没有全面上 TS,但在接口返回数据、订单状态枚举这些关键位置用了 JSDoc 注释和轻量类型定义,前后端字段对不上的问题少了很多。
1.3 MyBatis-Plus:把CRUD从“写代码”变成“配配置”
MyBatis-Plus 经常被误解成“只是简化 CRUD”,实际用下来它省掉的不只是简单增删改查。租赁系统里最常用的几个能力,按性价比排序如下:
- 内置
BaseMapper:单表 CRUD 完全不用写 SQL,selectById、insert、updateById开箱即用。 Wrapper条件构造器:动态查询条件不用写<if>标签拼 SQL,直接用LambdaQueryWrapper链式调用。- 分页插件:一套配置全局生效,不需要每写一个查询就手动拼 LIMIT。
- 逻辑删除:配置
@TableLogic后,删除操作自动变成 UPDATE,查询自动带deleted = 0条件,对租赁订单这种不能物理删除的强业务数据非常合适。 - 自动填充:
create_time、update_time字段通过 MetaObjectHandler 自动填充,不需要在业务代码里手动 set。
和 Spring Data JPA 对比,MyBatis-Plus 的优势在于 SQL 是可控的。租赁系统有一个核心查询是“查找某个时间段内可租赁的物品”,这种查询单靠方法名推导很难表达清楚,写 XML 里反而直观。JPA 的复杂查询虽然也可以写 @Query,但一旦涉及多表 join、动态条件、分页排序,还是 MyBatis 这一套更顺手。如果项目里的实体关系非常复杂且强关联,JPA 的级联操作可能更省事,但租赁系统的核心是订单和物品的管理,每个实体的边界很清楚,MyBatis-Plus 刚好卡在最舒服的位置。
1.4 MySQL8.0:默认字符集与JSON能力加持
MySQL8.0 相比 5.7 最重要的改进是默认字符集变成了 utf8mb4。这意味着你在建表时不需要刻意指定字符集,也能完整支持中文、emoji、生僻字。很多老项目在 MySQL5.7 时代被迫用 utf8mb4 + utf8mb4_unicode_ci 组合才能避免中文乱码,到 8.0 这里已经变成默认值。
MySQL8.0 的窗口函数也是我实际用到的点。租赁系统的后台统计需要计算“每个物品的累计租赁次数”,用窗口函数 ROW_NUMBER() 或 SUM() OVER(PARTITION BY ...) 写起来非常简洁,一次 SQL 就能完成分组排名,不用在 Java 内存里做二次聚合。另外 MySQL8.0 的 JSON 类型和函数(JSON_EXTRACT、JSON_TABLE)能处理一些“用户自定义物品属性”这类不固定字段的场景,虽然这个项目里我用了更简单的扩展字段设计,但 MySQL8.0 给我留下了足够的扩展空间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端核心实现:从租借闭环到 MyBatis-Plus 的实践经验
2.1 租赁业务的核心闭环
网上租赁系统和普通电商系统的最大区别是:物品可以重复租借,订单有明确的时间周期,钱的计算不只是商品价格,还涉及押金、租金、逾期费用、归还状态。所以后端业务模型必须围绕一个闭环来设计:
用户浏览物品 -> 用户发起租借 -> 系统锁定物品 -> 管理员审核 -> 用户支付押金和租金 -> 用户使用 -> 用户归还 -> 管理员确认归还 -> 系统退还押金或结算违约金。
这个闭环里每一个环节都对应一个状态字段,而且状态迁移是有方向的。比如订单状态我设计了这几个值:待审核、待支付、租赁中、逾期、已归还、已取消。状态机如果散落在业务代码里,改一处漏一处是常事。我的做法是在订单实体里维护一个 status 字段,同时在 Service 层写一个 OrderStatusTransition 枚举,把允许的状态流转关系定义清楚。
code复制public enum OrderStatus {
PENDING_REVIEW(0, "待审核"),
PENDING_PAYMENT(1, "待支付"),
RENTING(2, "租赁中"),
OVERDUE(3, "逾期"),
RETURNED(4, "已归还"),
CANCELLED(5, "已取消");
private final int code;
private final String desc;
}
这样在控制器里根据当前状态判断下一步操作是否合法,比到处写 if-else 清晰得多。
2.2 数据库表设计要点
租赁系统的表不算多,但每张表的字段都必须有明确归属。我实际设计时是这样的:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password, phone, role, status | 用户与管理员共用一张表,通过 role 区分 |
| item | id, name, category_id, daily_price, deposit, stock, cover, status | 物品表,租借状态和库存分开管理 |
| rent_order | id, order_no, user_id, item_id, start_date, end_date, total_price, deposit, status | 核心订单表,金额冗余存储 |
| payment_record | id, order_id, amount, type, status, create_time | 支付流水,押金和租金分别记录 |
| category | id, name, sort | 分类表,简单树形结构 |
设计时有三个点值得注意。第一,订单里的 total_price、deposit 这类金额字段必须冗余存储,不能下单时去实时查物品表,否则物品价格调整会影响历史订单。第二,订单编号不要用自增 ID,我用的是 yyyyMMddHHmmss + 用户ID后四位 + 随机数 生成唯一订单号,方便排查问题。第三,金额字段使用 DECIMAL(10,2),不要用 double,Java 实体里对应 BigDecimal,这是财务数据的基本要求。
2.3 MyBatis-Plus 的常用模式
MyBatis-Plus 的集成配置很简单,但细节决定体验。首先在 pom.xml 里引入依赖:
xml复制<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
然后在启动类或配置类里注册分页插件:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
这里有一个很容易被忽略的问题:分页插件必须显式传入 DbType.MYSQL,否则在部分场景下 LIMIT 语句的方言判断可能出错。我见过有人只加了 MybatisPlusInterceptor 没指定数据库类型,分页查询第一页正常,第二页就开始报 SQL 语法错误,排查半天才发现是这个原因。
实体类上的通用配置也建议统一约定。逻辑删除字段统一叫 deleted,自动填充字段统一叫 create_time 和 update_time,这样在实体类上加注解时非常整齐:
java复制@Data
@TableName("rent_order")
public class RentOrder {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
自动填充还需要一个 MetaObjectHandler 实现类:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
这样所有新增和修改操作都不用在 Service 层手动设置时间,节省的代码量看起来不多,但几十张表累计起来非常可观。
2.4 XML和Mapper同包配置,细节得讲清楚
租赁系统里物品搜索是一个相对复杂的多条件查询:关键词、分类、价格区间、可租状态、时间范围,还要分页排序。这种查询用注解 @Select 写起来会很长很乱,正确做法是把 SQL 写到 XML 文件里。但“XML 放哪里”这个问题坑过不少人。
最推荐的配置是:ItemMapper.java 接口和 ItemMapper.xml 放在同一个包目录下,比如 com.example.rent.mapper。然后通过 application.yml 指定 mapper 位置:
yaml复制mybatis-plus:
mapper-locations: classpath*:com/example/rent/mapper/**/*.xml
如果你用的是 Maven 标准目录结构,还要记得一个关键坑:src/main/java 目录下的 XML 文件不会被 Maven 默认打包进最终产物。所以需要在 pom.xml 的 build 节点里加一段资源配置:
xml复制<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</build>
不加这个配置的后果是:开发环境 IDE 里跑得好好的,一 mvn clean package 打出来的 jar 包就跑不起来,报 Invalid bound statement,因为 XML 文件根本没有进到包里去。这个问题在交付项目时特别致命,因为对方大概率不会用 IDE 跑,而是直接用 java -jar。
XML 里写 SQL 时,MyBatis-Plus 还有一个很实用的技巧:如果只是想用 MyBatis-Plus 的分页功能,但又想自己控制 SQL,可以把分页参数直接作为方法参数传入,XML 里不需要写 LIMIT:
xml复制<select id="selectItemPage" resultType="com.example.rent.entity.Item">
SELECT i.*, c.name AS category_name
FROM item i
LEFT JOIN category c ON i.category_id = c.id
<where>
<if test="keyword != null and keyword != ''">
AND (i.name LIKE CONCAT('%', #{keyword}, '%')
OR i.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND i.category_id = #{categoryId}
</if>
<if test="minPrice != null">
AND i.daily_price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND i.daily_price <= #{maxPrice}
</if>
</where>
ORDER BY i.create_time DESC
</select>
对应 Mapper 接口里返回 IPage<Item>,调用时传入 Page 对象即可。> 和 < 是因为 XML 里不能直接写大于号小于号,这个细节在交付文档里反复强调过,还是有人会漏。
3. Vue3 前端的组件化设计与前后端联调
3.1 从Vite脚手架到项目骨架
前端部分我选择 Vite + Vue3 + Element Plus + Pinia + Vue Router + Axios 这套组合。初始化命令很简单:
bash复制npm create vite@latest rent-frontend -- --template vue
Vite 相比 Webpack 在开发体验上的提升是质的飞跃,尤其是热更新速度。租赁系统页面数量不算多,但后台管理列表页经常要调样式,Vite 几乎秒级响应,比 Webpack 编译快了一个量级。
Element Plus 在表单和表格场景下非常顺手,我用的是按需导入模式,配合 unplugin-vue-components 和 unplugin-auto-import,自动引入组件和 API,不需要在代码里全局 import ElementPlus,打包体积也小很多。
状态管理用了 Pinia,因为它比 Vuex 更简单,而且对 TypeScript 支持更好。租赁系统里真正需要全局共享的状态不多,主要是当前登录用户信息和订单筛选条件,所以 Pinia 的 store 设计很轻量:
javascript复制import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null
}),
actions: {
setToken(token) {
this.token = token
localStorage.setItem('token', token)
},
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
}
}
})
3.2 Axios封装与鉴权设计
前后端分离架构下,Axios 封装是确保联调不混乱的关键。我在 utils/request.js 里统一做了四件事:设置 baseURL、请求拦截器携带 token、响应拦截器统一处理业务错误、超时设置。
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
if (res.code === 401) {
router.push('/login')
}
return Promise.reject(new Error(res.message))
}
return res
},
error => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default service
这里后端约定统一返回结构 { code, message, data },能省掉大量重复判断。401 状态码统一跳转登录页,也避免了很多“明明没登录,页面上所有按钮都能点”的尴尬。
登录鉴权我用的是 JWT,后端过滤器在每次请求时校验 token 有效性。SpringBoot2 里配置拦截器只需实现 HandlerInterceptor:
java复制public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("Authorization");
if (StringUtils.startsWith(token, "Bearer ")) {
token = token.substring(7);
}
// 校验并解析 token,若失败则抛出 401 异常
return true;
}
}
前端路由守卫里再做一次登录态检查,这种前后端双重校验的方式虽然有点重复,但能有效避免有人绕过前端直接调接口。
3.3 租赁流程页面的组件化拆解
前端页面按业务模块拆成了几块:公共布局、首页、物品列表、物品详情、订单确认、订单管理、个人中心、后台管理。以物品列表为例,它看起来只是一个页面,实际可以拆成四个组件:SearchBar、ItemCard、Pagination、FilterPanel。
组件化的收益要到维护期才体现得出来。租赁系统后期加了一个“按价格排序”功能,我只需要在 SearchBar 里加一个下拉框,再把排序参数传给列表请求,完全不用碰 ItemCard 和分页组件。如果所有的逻辑都堆在一个页面里,一次功能变更就要细读几百行代码。
订单确认页是租赁系统特有的交互场景,比普通电商要复杂:用户选择物品后要选起止日期,系统要实时计算租金、押金和逾期规则。日期选择我用 Element Plus 的 el-date-picker,并限制了起租日期不能小于今天,结束日期必须大于起租日期。计算逻辑抽成了一个 useOrderCalculator 组合式函数,接收物品单价、押金、起止日期,返回总价和租借天数。这样单价计算逻辑可以被订单确认页和订单详情页复用。
3.4 联调中必踩的三个隐藏坑
前后端联调是我这个项目里最耗时的一环,绝大多数问题都和代码无关,而是环境配置。
第一个坑是跨域。开发环境最常见的做法不是在后端加 CORS,而是在 Vite 配置文件里设置代理:
javascript复制export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
这样前端请求 /api/user/login 会被代理到 http://localhost:8080/user/login,浏览器层面不产生跨域。后端代码里就不用到处写 @CrossOrigin 了。生产环境则用 Nginx 做反向代理,把 /api 转发到后端服务。
第二个坑是 Long 类型 ID 的精度丢失。MySQL 自增主键和表 ID 都是 bigint,后端实体里对应 Long,但前端 JavaScript 的 Number 类型在超过 2^53 - 1 后会丢失精度。订单表的数据一旦超过几万条,ID 就可能变成一个奇怪的数字。解决办法是让后端统一返回字符串格式,SpringBoot 里配置 Jackson:
yaml复制jackson:
generator:
write-numbers-as-strings: true
或者在每个实体的 ID 字段上加 @JsonFormat(shape = JsonFormat.Shape.STRING)。这个坑在本地测试数据少时根本看不出来,数据一多才炸,属于典型的交付后问题。
第三个坑是时间格式。后端返回 LocalDateTime 默认序列化成 ISO 格式,前端 Element Plus 的表格列直接显示会有一长串 T 字分隔符,很难看。我在 application.yml 里统一配置:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
前端再用 dayjs 做格式化,两边就对齐了。这里要特别注意时区设置,不配置 time-zone: GMT+8 的话,数据库存的是北京时间,返回给前端的却会差 8 个小时。
4. MySQL8.0 的安装、连接与运行参数,别在这里翻车
4.1 本地安装与Docker安装怎么选
MySQL8.0 的安装方式无非两种:本地安装和 Docker 容器化。本地安装适合开发阶段,直接去官网下载安装包,一键装完还能用系统服务管理,适合不想接触命令行的人。Docker 安装则适合交付场景,因为可以打包一个固定的 MySQL 8.0 环境,保证对方部署时不会因为操作系统差异出问题。
我实际交付时推荐对方使用 Docker 安装,命令很简单:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root123456 \
-e TZ=Asia/Shanghai \
-v /opt/mysql8/conf:/etc/mysql/conf.d \
-v /opt/mysql8/data:/var/lib/mysql \
mysql:8.0
这里有两个参数容易被忽略。一个是 -e TZ=Asia/Shanghai,不设置的话容器默认是 UTC 时区,数据库时间会比北京时间慢 8 个小时,之后所有订单时间、归还时间都会错,排查起来极其痛苦。另一个是挂载数据目录,如果不挂载,容器删除后数据全部丢失,对一个租赁系统来说等于所有订单记录都没了。
4.2 连接串里每个参数的含义
SpringBoot 连接 MySQL8.0 的 JDBC URL 应该是这个形式:
code复制jdbc:mysql://localhost:3306/rent_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
参数看起来多,但每个都有原因:
useSSL=false:MySQL8.0 默认开启 SSL 认证,但在本地开发环境没有必要,加上这个参数能避免一大堆 SSL 握手警告。serverTimezone=Asia/Shanghai:8.0 连接驱动默认使用服务器时区,不指定的话可能会报Server returns invalid timezone错误。allowPublicKeyRetrieval=true:MySQL8.0 默认使用caching_sha2_password认证插件,客户端连接时需要先从服务器获取公钥,不加这个参数会报Public Key Retrieval is not allowed。characterEncoding=utf8:确保传输链路编码正确,防止中文乱码。
如果你是直接用 Navicat 连接 MySQL8.0,也要注意:Navicat 15 之前的版本对 caching_sha2_password 插件支持不完善,可能连接报错。解决办法是创建一个使用 mysql_native_password 插件的用户,或者在连接属性里设置 allowPublicKeyRetrieval=true。MySQL8.0 默认的认证插件比 5.7 更安全,但兼容性就是这个代价。
4.3 字符集、排序规则与大小写敏感的取舍
MySQL8.0 默认字符集已经是 utf8mb4,但创建数据库时我还是建议显式写出来,避免“默认配置在别人机器上不同”导致的差异:
sql复制CREATE DATABASE `rent_system` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
排序规则 utf8mb4_general_ci 和 utf8mb4_unicode_ci 的区别在于:general 排序速度更快,unicode 排序更精确、支持更多语言字符。租赁系统这种中文字段为主的项目,general 就完全够用了。
大小写敏感是另一个容易踩的坑。MySQL 在 Linux 下默认 lower_case_table_names=0,表名区分大小写;在 Windows 和 macOS 下默认是 1,不区分。如果你在 Windows 上开发时建的表名是 Item,部署到 Linux 服务器上 SQL 写成了 item,就会报 Table doesn't exist。我建议所有表名、字段名统一用小写加下划线风格,比如 rent_order、daily_price,全程保持一致,彻底避开这个问题。
4.4 初始化数据导入与常见报错
系统交付时必须附带一份完整的初始化 SQL,包含建表语句、基础分类数据、演示物品和测试账号。导入命令:
bash复制mysql -u root -p rent_system < rent_system.sql
如果是 Docker 容器里的 MySQL:
bash复制docker exec -i mysql8 mysql -u root -p root123456 rent_system < rent_system.sql
常见报错有两种。一种是 SQL 文件里有 DELIMITER 语法的存储过程,直接用 Navicat 导入会报错,最好用命令行导入。另一种是导入时提示 Unknown database,这是因为 SQL 文件里没有 USE rent_system; 语句,你需要先手动创建数据库再导入。
5. 交付前的最后几步:文档、演示数据与体检清单
5.1 把“怎样跑起来”写清楚,比代码更重要
这套源码带了一个“含文档”标签,实际写文档的时候我深刻体会到:文档不是代码的附属品,而是交付物的灵魂。
一份真正能让人跑起来的 README 至少要包含这几部分:项目简介、技术栈版本、环境要求(JDK8、Node 16+、MySQL8.0)、初始化步骤(创建数据库、导入 SQL、修改配置、启动后端、启动前端)、默认账号、常见问题。其中“环境要求”必须写清楚版本号,比如 Node 版本太低时 Vite 4.x 可能启动报错,Node 太高时老版本 npm 包又可能有兼容问题。
我把后端启动命令和前端启动命令都写成了可以直接复制的形式,连 npm install 的提示都写上。因为在交付场景下,对方大概率不会关心你的业务设计思路,只关心一件事:能不能一键跑起来。
5.2 初始化数据与演示账号
一个空的租赁后台管理页面很难看出系统价值,所以我专门准备了一套演示数据:10 个分类、30 个物品、若干条不同状态的租赁订单,以及两个测试账号。测试账号分角色:
| 账号 | 密码 | 角色 | 用途 |
|---|---|---|---|
| admin | admin123 | 管理员 | 查看后台管理页面、审核订单、管理物品 |
| user | user123 | 普通用户 | 浏览物品、发起租赁、查看订单 |
演示数据还有一个隐藏价值:方便对方快速验证系统的状态流转。比如我预置了一条“租赁中”的订单,对方可以直接点击“确认归还”查看整个状态变化过程,不用自己从头下单,体验远好于对着空数据库点点点。
5.3 交付前的体检清单
每次交付源码之前,我会按固定顺序过一遍体检清单,避免出现最尴尬的“发出去就打不开”:
- [ ] 后端能否用
mvn clean package完整打出 jar 包,且不依赖 IDE - [ ] 前端能否用
npm run build完整构建,产物里有明确的dist目录 - [ ] 数据库初始化 SQL 能否在一个全新的 MySQL8.0 环境上导入成功
- [ ] 整个流程的默认账号密码是否在 README 里写明
- [ ] 前后端接口的 baseURL 是否和部署环境一致,代理配置是否生效
- [ ] 打包后的产物是否包含所有 XML 文件,尤其确认 mapper-locations 对应的文件都在
- [ ] 是否存在写死的本地路径、测试数据、不应公开的密钥
- [ ] 版本号是否统一标注在 pom.xml 和 package.json 中,方便对方定位问题
这套清单看起来琐碎,但每一个都是过去交付翻车换来的教训。比如 mvn clean package 打出来的包能不能跑,这一点开发时几乎不会被验证,因为 IDE 里跑和 jar 包跑的环境差异大,只有认认真真 clean 一次、再 java -jar 启动一次,才能确认整个构建链路没问题。
我个人在实际操作中最推荐的交付顺序是:先写 README,再初始化数据库,最后再跑前后端。因为写文档的过程会逼着你在全新环境里过一遍所有步骤,这个“重新走一遍”的动作比任何测试都能暴露问题。所谓“含文档”的源码,真正的含金量不在于文档页数多寡,而在于别人按照文档操作时,每一步是不是都能原样复现。做到这一步,这套 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的网上租赁系统才算真正达到了可以交付的状态。
