做过不少Java Web项目之后,你会发现SpringBoot + Vue + MyBatis + MySQL这套组合在中小型管理系统里依然是最稳、最省心的搭配。最近刚好把一套房屋租赁管理系统从零到一完整跑通,从架构设计、表结构、接口划分到前端联调、部署上线,整个流程里踩了不少坑,也沉淀了不少经验。这篇就把整个项目拆开讲,给准备做同类系统(毕业设计、课设、或者小公司内部系统)的朋友一个可以直接抄作业的参考。
这个系统覆盖了租赁业务的完整闭环:房东发房源、管理员审核、租客浏览签约、账单缴费、报修处理、退租结算全都有。技术层面不算难,但胜在五脏俱全——前端Vue + Element UI做界面,后端SpringBoot提供接口,MyBatis操作MySQL,三层架构清晰。如果你手上正缺一个能快速跑起来、逻辑完整的管理系统项目做练手或交付,这套东西的骨架子非常值得参考。
1. 项目整体设计:先搞清楚它到底要解决什么问题
1.1 核心角色和业务闭环
做任何项目,第一步永远是理清角色和业务流程,而不是急着建表、写代码。房屋租赁管理系统的核心角色有3个:管理员、房东、租客。我第一次做这类系统时就吃过亏——上来就埋头设计表结构,结果做到一半发现业务流程根本推不动,返工了两次。所以下面这个闭环必须先想清楚:
- 房东登录后发布房源(上传图片、地址、价格、面积、户型等);
- 管理员审核房源,审核通过后房源才会公开上架;
- 租客浏览房源、收藏、提交租赁申请;
- 房东确认申请后生成租赁合同,记录起止时间、租金、押金;
- 系统根据合同周期自动生成账单(房租、水电费等),租客在线确认缴费;
- 租客入住后可以发起报修,房东处理并反馈;
- 合同到期可以续约或退租,退租时结算押金和欠费。
这一套流程里,权限边界很清晰:租客只能操作自己的申请和账单,房东只能管理自己的房源和租客合同,管理员拥有最高权限。如果你只是做一个演示级别的项目,可以简化掉部分功能,但角色权限这层建议保留,因为在简历或答辩里这是一个很容易被追问的点。
1.2 技术选型逻辑:为什么还是SpringBoot + Vue + MyBatis
我知道有人会说,现在微服务、前后端分离都这么流行了,为什么还要用这套"老组合"?但实际下来,这套组合在中小型项目里确实是最优解,我说几个你写代码时能实际感受到的理由:
- SpringBoot把配置简化到极致,不需要复杂XML,一个Application类就能启动整个服务,开发效率很高。
- Vue的双向数据绑定和组件化开发,写后台管理界面比JSP时代爽太多了,Element UI拖过来就能用,页面效果很拿得出手。
- MyBatis半自动ORM在手写复杂查询时非常灵活,比如房源列表的多条件筛选,动态SQL能直接在XML里搞定。
- MySQL部署简单,大部分人本地环境现成就有,不用为数据库引入额外学习成本。
如果你追求"开箱即用"的效果,这套组合能让你至少节省一半的开发时间。反过来说,这个项目里我之所以没有用Spring Data JPA,是因为租赁系统的查询条件组合比较多——价格区间、区域筛选、面积范围、发布时间排序,这些用MyBatis动态SQL比JPA派生的查询方法直观得多。
2. 数据库设计:六张核心表撑起整个业务
2.1 表结构总览
数据表设计是这个系统的地基,表建好了,后面所有功能都是在上面添砖加瓦。我最终拆了6张核心表,把之前很多人在设计中容易犯的"一张表塞所有字段"的毛病直接避开。下面这张表是整体概览,建议据此做数据库脚本。
| 表名 | 表用途 | 关键字段说明 |
|---|---|---|
| sys_user | 用户表 | id, username, password, role, phone, nickname, avatar, create_time |
| house | 房源表 | id, landlord_id, title, address, region, price, area, bedroom, hall, imgs, status, audit_status, create_time |
| lease | 合同表 | id, house_id, tenant_id, landlord_id, start_date, end_date, rent_month, deposit, status, create_time |
| bill | 账单表 | id, lease_id, type, amount, status, deadline, pay_time |
| repair | 报修表 | id, house_id, user_id, content, status, feedback, create_time, handle_time |
| favorite | 收藏表 | id, user_id, house_id, create_time |
6张表不是拍脑袋定的,每张表都对应一个明确的业务动作。sys_user承载登录和角色区分;house承载租赁的"物";lease把"人"和"物"关联起来;bill承载资金流水;repair承载售后服务闭环;favorite承载租客的前期意向。你在设计自己的系统时也遵循这个原则:每个核心业务动作最好都是一张独立的表,不要揉在一起。
2.2 关键表的设计细节和字段取舍
先看house表,这是整个系统里信息量最大的表。除了常规的title、address、price、area之外,有几个字段值得特别注意:
audit_status:这是房源的审核状态,用0/1区分待审核和已通过。很多人会把审核状态和上架状态合并成一个status字段,看起来省事,但实际运营中管理员可能需要先审核、再上架两步操作,拆开才能在页面上做"待审核列表"和"已上架列表"两个视图。imgs:房源图片字段,我用JSON字符串存储多张图片路径,这样实现图片轮播很简单。如果你用MySQL 5.7以下版本,可以用逗号分隔,但处理起来稍麻烦。region:区域字段单独拎出来,方便后续做区域筛选。虽然也可以从address里模糊匹配,但查询效率低,而且区域数据不干净的时候筛选结果会很尴尬。
lease表是另一个重点。租期字段用start_date和end_date精确到天,不要只存一个租期数字,因为系统要根据这两个字段自动计算账单和判断合同状态。status字段建议用int类型,规则约定:1待确认、2生效中、3已到期、4已退租。在我的实现里,0不需要,因为合同从创建的瞬间就要进入1状态。
bill表的type字段区分租金和水电费,amount字段直接存金额,不要存单价和数量再计算,因为水电费计算规则随时可能改,存最终金额才能保证历史账单不被规则变更影响。
提示:建表时统一使用InnoDB引擎和utf8mb4字符集。InnoDB保证事务和行级锁,utf8mb4保证用户输入的任何特殊字符(emoji表情)都不会报错,踩过这个坑的都懂。
2.3 外键和索引怎么处理
很多学生项目喜欢建表时顺手把外键加上,但在实际项目里,我建议别加物理外键,而是用代码保证逻辑关联。为什么?因为物理外键会让数据迁移、批量删除变得非常痛苦,而且性能上有额外开销。这6张表本身业务关联不太复杂,逻辑外键足够了。
索引的取舍上,几个高频查询字段必须加索引:
ALTER TABLE house ADD INDEX idx_h_landlord (landlord_id); ALTER TABLE house ADD INDEX idx_h_status (status, audit_status); ALTER TABLE lease ADD INDEX idx_l_tenant (tenant_id); ALTER TABLE lease ADD INDEX idx_l_house (house_id);这是我从一个"系统越跑越慢"的教训里提炼出来的:数据量没到几十万条之前,单表查询加索引后的性能表现完全够用,不需要过早引入缓存或分库分表。
3. 后端核心模块实现:SpringBoot + MyBatis的关键代码
3.1 三层架构与接口规划
后端按照controller - service - mapper三层去拆。Controller只负责参数接收和结果返回,Service里写业务逻辑,Mapper只管数据库读写。这个分层看起来基础,但真的能保证项目不乱——尤其是当功能越来越多时,没有分层就像把所有衣服堆在一个衣柜里。
核心接口规划如下:
| 模块 | 接口 | 功能说明 |
|---|---|---|
| 用户模块 | POST /api/user/login | 登录,返回token |
| 用户模块 | POST /api/user/register | 注册,默认角色租客 |
| 房源模块 | GET /api/house/list | 条件分页查询房源 |
| 房源模块 | POST /api/house/add | 房东新增房源 |
| 房源模块 | GET /api/house/auditList | 管理员查看待审核列表 |
| 房源模块 | POST /api/house/audit | 管理员审核房源 |
| 合同模块 | POST /api/lease/apply | 租客发起租赁申请 |
| 合同模块 | POST /api/lease/handle | 房东处理申请 |
| 合同模块 | POST /api/lease/renew | 续约 |
| 账单模块 | GET /api/bill/list | 查看个人账单 |
| 账单模块 | POST /api/bill/pay | 账单缴费 |
| 报修模块 | POST /api/repair/add | 提交报修 |
登录我用了JWT(JSON Web Token)而不是Session,因为前后端分离的项目里,前端Vue和后端SpringBoot很可能部署在不同的端口甚至服务器,Session的跨域处理要配置很多东西,JWT天然无状态,前端把token存到localStorage里,请求时带上Authorization头即可。
3.2 登录接口和JWT工具类的实现
JWT的核心逻辑不复杂,生成token时把用户id和角色放进去,后续每个请求都通过拦截器解析token拿到当前用户信息。下面这段是生成token的工具方法:
public String generateToken(Long userId, String role) { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR, 24); // token过期时间24小时 Date expireDate = calendar.getTime(); return Jwts.builder() .setSubject(userId.toString()) .claim("role", role) .setIssuedAt(new Date()) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }登录接口的逻辑是:前端传username和password -> 后端查用户 -> 密码用MD5加密后比对(真实项目建议升级为BCrypt)-> 成功则返回token和用户信息。要注意密码绝对不能在日志里打印,我调试时吃过一次亏,把日志文件给了别人,里面明文密码全暴露了。
拦截器是JWT方案里的关键一环:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(401); return false; } try { Claims claims = Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token.substring(7)) .getBody(); request.setAttribute("userId", Long.parseLong(claims.getSubject())); request.setAttribute("role", claims.get("role")); return true; } catch (Exception e) { response.setStatus(401); return false; } }这里我特别说明一下为什么要用request.setAttribute把userId存起来,而不是每次在Service里重新解析token。因为一次请求链路里Controller和Service可能多次需要当前用户信息,每次都解析浪费时间,一次解析存到request域,后续直接取就行。这是一个很小的性能优化,但在高并发接口里体现很明显。
3.3 MyBatis动态SQL:多条件房源查询
房源列表页是最典型的动态SQL场景:用户可以选择区域、价格区间、面积范围,还能按发布时间排序。如果用静态SQL,光是应付条件组合就要写七八条SQL语句,维护成本很高。MyBatis的<where>和<if>标签完美解决了这个问题。
<select id="selectHouseList" resultType="com.example.entity.House"> SELECT h.*, u.nickname AS landlord_name FROM house h LEFT JOIN sys_user u ON h.landlord_id = u.id <where> <if test="region != null and region != ''"> AND h.region = #{region} </if> <if test="minPrice != null"> AND h.price >= #{minPrice} </if> <if test="maxPrice != null"> AND h.price <= #{maxPrice} </if> <if test="area != null"> AND h.area >= #{area} </if> <if test="keyword != null and keyword != ''"> AND (h.title LIKE CONCAT('%', #{keyword}, '%') OR h.address LIKE CONCAT('%', #{keyword}, '%')) </if> AND h.audit_status = 1 AND h.status = 1 </where> ORDER BY h.create_time DESC </select>有几个细节是新手容易踩坑的:
第一,>和<是XML转义符,不能直接写>和<,否则XML解析直接报错。第二,CONCAT拼接模糊查询比直接写'%${keyword}%'安全得多,因为${}是字符串替换,存在SQL注入风险,而#{}是预编译占位符。第三,<where>标签会自动去掉第一个多余的AND,但如果条件全为空,它不会生成WHERE关键字,这是一个非常优雅的特性。
分页我用的是PageHelper插件,用法很简单:
PageHelper.startPage(pageNum, pageSize); List<House> list = houseMapper.selectHouseList(query); PageInfo<House> pageInfo = new PageInfo<>(list);PageHelper通过拦截器在SQL执行前自动拼接LIMIT,返回值里自带total、pageNum、pageSize等分页数据,前端做分页组件时直接取total就行。
3.4 事务处理:租赁申请和账单生成的一致性
租赁申请是整个系统里最需要事务保障的业务。租客提交申请后,系统要做三件事:创建合同记录、更新房源状态为"已申请"、生成首期账单。这三个操作要么全成功,要么全失败,只成功一半就会出现"合同已建但房源还能被其他人申请"的脏数据。
SpringBoot处理事务非常方便,在Service方法上加@Transactional注解即可:
@Transactional(rollbackFor = Exception.class) public Integer applyLease(LeaseApplyRequest request) { // 1. 校验房源状态,防止并发重复申请 House house = houseMapper.selectById(request.getHouseId()); if (house == null || house.getStatus() != 1) { throw new RuntimeException("房源不存在或不可申请"); } // 2. 创建合同 Lease lease = new Lease(); lease.setHouseId(request.getHouseId()); lease.setTenantId(request.getTenantId()); lease.setLandlordId(house.getLandlordId()); lease.setStartDate(request.getStartDate()); lease.setEndDate(request.getEndDate()); lease.setRentMonth(house.getPrice()); lease.setDeposit(house.getDeposit()); lease.setStatus(1); leaseMapper.insert(lease); // 3. 更新房源状态 houseMapper.updateStatus(request.getHouseId(), 2); // 4. 生成首期账单 Bill bill = new Bill(); bill.setLeaseId(lease.getId()); bill.setType(1); // 1-租金 bill.setAmount(house.getPrice()); bill.setStatus(0); // 0-待支付 billMapper.insert(bill); return lease.getId(); }一个容易忽略的点是rollbackFor = Exception.class。如果不指定这个参数,Spring默认只在RuntimeException时回滚,检查异常(比如FileNotFoundException)不会触发回滚。实际业务里很多异常是自定义的运行时异常,但为了保险起见,建议一律显式指定。
注意:事务方法里不要catch掉异常然后又吞掉,否则事务根本不会感知到错误,也就不会回滚。正确做法是让异常抛出去,或者catch后手动调用
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
4. 前端Vue实现:从页面搭建到接口联调
4.1 Vue项目的目录结构
前端这块我用的是Vue2 + Element UI,虽然不是最新的Vue3,但胜在生态成熟稳定,对管理系统来说组件库丰富、资料多、上手快。如果你没有历史包袱,直接上Vue3 + Element Plus也是可以的,核心逻辑差别不大。
前端目录结构按模块划分:
src/ api/ // 所有接口请求封装 user.js house.js lease.js bill.js router/ // 路由配置 index.js store/ // Vuex状态管理 index.js views/ login/ // 登录注册页 home/ // 首页房源列表 house/ // 房源详情 admin/ // 管理后台 tenant/ // 租客个人中心 landlord/ // 房东工作台 components/ // 公共组件 HouseCard.vue我建议所有接口请求统一放在api目录里封装,比如house.js里导出所有房源相关接口函数。这样页面组件里不直接出现axios调用,后期接口地址变了只需要改api目录,全局搜索一下就好了。
4.2 路由守卫实现登录校验
Vue前端做登录拦截的核心是路由守卫。前端这边的拦截逻辑要和后端配合:没有token就跳转登录页,有token但访问了无权限的页面也要拦截。以下是我的实现思路:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token'); if (to.path === '/login') { next(); return; } if (!token) { next('/login'); return; } // 角色权限控制 const role = localStorage.getItem('role'); if (to.meta.roles && !to.meta.roles.includes(role)) { // 没有权限访问,跳转到首页 next('/'); return; } next(); });to.meta.roles在路由配置里定义,比如管理后台的路由加上roles: ['admin'],房东工作台路由加上roles: ['landlord']。这套前端路由守卫配合后端拦截器,实现了双层防护。我强调一下:前端路由守卫只是改善用户体验的,真正的安全防线必须靠后端,前端绕过路由守卫太简单了。
4.3 房源列表页的筛选和分页实现
房源列表页是租客看到的第一屏,体验好坏直接影响整个项目印象。我用的方案是:搜索表单 + 房源卡片列表 + 分页组件。
搜索表单绑定一个查询对象,提交时调用API接口:
searchParams() { return { region: this.region, minPrice: this.minPrice, maxPrice: this.maxPrice, keyword: this.keyword, pageNum: this.currentPage, pageSize: 8 }; }, loadHouseList() { getHouseList(this.searchParams).then(res => { this.houseList = res.data.list; this.total = res.data.total; }); }这里有一个经验:前端传给后端的参数名要能直接映射到MyBatis的Mapper参数里,减少一层参数转换。比如前端字段是minPrice,Mapper里也是minPrice,就不用再写一堆map.put("minPrice", xxx)。
房源卡片组件里展示图片、标题、价格、面积、区域,点击跳转到详情页。详情页再展示大图轮播、户型描述、联系房东按钮、申请租房按钮等。
4.4 管理后台的表格页面
管理后台是我觉得这套系统里最"省力"的部分,因为Element UI的el-table+el-pagination+el-dialog三件套基本就能搞定所有页面。用户管理、房源审核、账单列表,本质都是同一个模式:表格展示数据 + 搜索 + 操作按钮(通过/拒绝)。
以房源审核为例,PageHelper返回的total直接绑到el-pagination上,按钮点击调审核接口:
handleAudit(row, status) { auditHouse({ id: row.id, auditStatus: status}).then(() => { this.$message.success(status === 1 ? '审核通过' : '已拒绝'); this.loadAuditList(); }); }这里需要注意审核操作的幂等性:后端接口里,审核前要判断房源当前状态,如果已经是已审核状态,就不能再次审核,否则会出现重复操作。前端也要在点击后立即禁用按钮,防止用户快速双击导致重复请求。
4.5 axios请求封装
axios封装是个绕不开的基础工作。我习惯把所有请求的公共逻辑抽到一个request工具类里,包括baseURL设置、请求头带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) { this.$message.error(res.message); if (res.code === 401) { localStorage.clear(); router.push('/login'); } return Promise.reject(new Error(res.message)); } return res; }, error => { if (error.response && error.response.status === 401) { localStorage.clear(); router.push('/login'); } return Promise.reject(error); } );后端统一返回格式{code: 200, message: "success", data: ...},前端在响应拦截器里把code判断抽出来,所有页面里的then回调拿到的直接就是data数据,不用每个页面都判断code。同时在后端拦截器返回401时,前端自动清除本地token并跳转登录页,这是处理登录过期最省心的方案。
5. 部署上线:SpringBoot打包和Vue构建
5.1 SpringBoot项目打包
项目完成后要部署,第一步是把后端打成jar包。SpringBoot的打包非常简单,但有几个配置细节容易忽略。
先在pom.xml里确认打包插件:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>然后在application.yml里把数据库连接、端口等配置放到环境对应的profile里。我习惯区分application-dev.yml(本地开发)和application-prod.yml(生产环境),启动时通过--spring.profiles.active=prod切换。
打包命令:
mvn clean package -DskipTests打出来的jar包在target目录下,直接运行:
java -jar house-lease-system.jar --spring.profiles.active=prod这里我强调一个部署细节:生产环境的MySQL密码不要明文写在配置文件里,至少也应使用环境变量占位:
spring: datasource: url: jdbc:mysql://localhost:3306/house_lease?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: ${DB_USERNAME} password: ${DB_PASSWORD}启动时通过环境变量传入:
DB_USERNAME=root DB_PASSWORD=yourpassword java -jar house-lease-system.jar5.2 Vite打包和SpringBoot整合
前端打包分两种情况:一种是前后端完全分离部署,前端打包出的静态文件扔到Nginx里,配合后端接口反向代理;另一种是把前端打包后的dist目录直接放进SpringBoot的src/main/resources/static下,让SpringBoot同时提供静态资源服务。对于中小型项目,第二种方案要省事得多。
先执行前端打包:
npm run build打包完成后,dist目录拷到SpringBoot项目的resources/static下,重启后端就内外网都能访问了。有几个注意点:
- 前端接口的baseURL不能写死成localhost,要写成相对路径或者和后端相同域名的API路径。比如由Nginx转发的,写
/api就好。 - 检查dist里的index.html引用的静态资源路径是相对路径
./而不是绝对路径/,不然部署到二级路径会白屏。
5.3 MySQL部署和数据导入
生产环境的MySQL安装我踩过不少坑。在Linux上安装MySQL,最稳的方式是使用官方yum仓库或者docker:
docker run -d --name mysql8 -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=yourpassword \ -e MYSQL_DATABASE=house_lease \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0用docker部署MySQL的优点是隔离干净、备份方便,数据目录挂载在宿主机上,容器随便删了重建数据都不丢。这个方案我在多个项目上实测,比裸机rpm安装省心太多。
建库建表后执行初始化数据脚本,把管理员账号init进去。管理员账号密码建议用BCrypt加密后存储,不要用明文。你可以写一个简单的初始化类,在SpringBoot启动时检测到user表为空就自动插入管理员账号,这样交付时省去手动操作的麻烦。
6. 常见问题与排查技巧实录
6.1 跨域问题
前后端分离开发时,最常见的第一个报错就是跨域。前端访问后端接口,浏览器报CORS错误。解决方式在后端加一个CORS配置类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(Registry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }要注意的是allowCredentials(true)必须和allowedOriginPatterns("*")配合,不能单独用allowedOrigins("*"),否则JDK高版本会报错——这是SpringBoot 2.4+之后的严格校验。另外,配置了JWT拦截器之后,预检请求(OPTIONS)也要直接放行,否则前端正式请求之前就挂在预检上了。处理方法是拦截器里判断请求方法,OPTIONS请求直接返回true。
6.2 MyBatis日志不打印SQL
调试阶段最需要看的就是MyBatis执行的SQL语句,方便定位问题。在application.yml里加:
mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这个配置会把每条SQL及参数值直接打印到控制台,是排查SQL问题的首选工具。我排查一个联表查询查不出数据的问题时,就是靠看打印出来的SQL发现在ON条件里顺手加了一个错误的过滤条件,导致结果集为空。定位效率非常高。
6.3 数据库连接报SSL错误
MySQL 8.0以上版本默认开启SSL,连接时如果没配置会报:
SSL connection error: The server requested SSL, but the client did not provide it.解决办法是在JDBC URL里添加:
url: jdbc:mysql://localhost:3306/house_lease?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true这个参数也很重要,MySQL 8.0使用caching_sha2_password认证插件时,没这个参数也会连接失败。这两个参数属于高频踩坑点,写进标准配置里就避免了后续反复折腾。
6.4 前端npm install报错
Vue项目clone下来后执行npm install成功但启动报错,大概率是Node版本和依赖的兼容性问题。Element UI 2.x版本在Node 18以上会有启动警告和奇怪报错。我的建议是固定开发环境的Node版本,我长期用的方案是Node 16.14,搭配Vue2项目的兼容性最稳。也可以装nvm管理Node版本,随时切换。
6.5 后端启动端口占用
SpringBoot默认端口8080,如果本地已经被其他Java进程占用,启动会直接报端口冲突。常规做法是在application.yml里改一个端口,但我更推荐用--server.port=参数动态指定端口:
java -jar house-lease-system.jar --server.port=8081这样本地开多个服务互不干扰,尤其调试多个项目并存的场景下很实用。
写在最后的几点个人体会
这些功能全部做完之后,我回过头来看整个项目,最大的体会是:这种业务管理系统的技术难度不在于哪个技术点特别深,而在于把每个环节稳稳当当地串起来。从前端表单的字段校验,到后端接口的参数校验,再到数据表字段的合理性,任何一环出现疏漏,最终都会以用户看得见的问题反馈出来。
这套系统我实际迭代了三轮才稳定下来。第一轮实现了基础CRUD,第二轮补充了审核流程和权限控制,第三轮优化了账单生成逻辑和数据统计。每一轮改动都有明确的目标,没有盲目堆功能。如果你打算做类似的系统,我也建议你有节奏地迭代,不要想着一次做到尽善尽美。
另外再分享一个小技巧:像这种SpringBoot + Vue的管理系统,最后交付时建议配一个自启动的运维脚本,把MySQL、后端、前端服务的启动命令写进去,这样换一台新机器部署时不用一条一条命令敲,省下大量时间。脚本本身不复杂,十几行bash脚本的事,但客户体验完全是两个档次。