从去年下半年开始,陆陆续续帮不少准备毕业设计的学弟学妹看过他们的项目,发现“校园体育器材租赁”这个题目出现频率相当高。这类系统的业务逻辑本质上就是一套“物品借还”流程,但加上器材种类、押金、租期、报修、统计这些约束之后,会立刻变得复杂起来。今天我就用一套基于Vue和SpringBoot的前后端分离实现方案,把整套系统从设计到落地的思路捋一遍,包括为什么这么拆模块、数据库表怎么建、核心接口怎么写、前端页面怎么组织,以及我在实际开发中踩过的坑和排查过程。无论你是拿它做毕设,还是想给自己的小项目做个管理后台,这套内容都很值得参考。
1. 整体设计与技术选型思路
1.1 为什么是 Vue + SpringBoot 这个组合
先说结论:这套组合是目前中小型管理和信息类项目里性价比最高的搭配,没有之一。
SpringBoot负责后端,它的核心价值在于“约定优于配置”。你用SpringBoot搭一个RESTful服务,不需要像早期SSH框架那样堆一堆XML配置,依赖管理也由Maven或Gradle统一搞定。快速创建一个项目,核心起步配置很少,写几个带注解的类就能跑起一套HTTP服务,非常适合单人开发周期在两到三个月内的毕设场景。
Vue负责前端,它最大的优势是响应式数据绑定和组件化开发。管理系统的界面大多是基于“表单+表格+弹窗”的组合,Vue的双向绑定能极大减少操作DOM的工作量,组件化的组织方式也方便把器材卡片、订单列表、统计图表这些重复出现的UI拆开复用。
最关键的是,前后端分离这种架构能让你把两端的进度拆开推进。我见过很多同学把业务代码全部写在JSP或者Thymeleaf页面里,结果改一个页面渲染逻辑要重启整个应用,调试效率很低。而分离之后,前端只需要调用后端提供的接口文档,后端只需要保证接口返回结构化数据,两边并行推进,联调阶段再做对接,出问题的时候定位起来也快得多。
1.2 业务角色的边界要一开始就划清楚
体育器材租赁系统表面看就是一个“租借管理”,但一旦涉及角色的差异,设计复杂度会立刻上来。我在拆解需求时,通常把用户划分为三类:
- 普通用户(学生/会员):搜索浏览器材、发起租赁、查看个人订单、归还器材、发起报修。
- 器材管理员:负责器材入库、出库、状态维护(可租/已租/维修中/已报废)、处理归还验收和报修审核。
- 系统管理员:负责用户管理、角色权限分配、查看统计报表(租赁热度、器材利用率、收入与损耗等)。
划清楚角色边界后,所有模块的归属就明确了。普通用户面对的是“前台”,重点是流程顺滑,操作路径短,类似电商的下单体验;管理端面对的是“后台”,重点是信息的完备和状态流转的可控,比如管理员必须看到每件器材的完整生命周期记录。
如果你一开始就只想着“能做出来”,把用户表里加一个admin字段就开始写代码,后面权限相关的需求一多,拦截器、菜单显示、接口授权都得返工。
1.3 项目结构怎么搭:按模块分包,不按层分包
我见过很多项目按“controller、service、dao、entity”这种技术层分包,小项目看起来干净,但功能一多就会变成灾难。器材管理相关的类、租赁业务相关的类、系统权限相关的类全都混在一起,新开发时找人找不到,改需求时牵一发动全身。
这里我推荐按“业务域”分包:
com.example.equipmentrental ├── common // 通用配置、工具类、统一返回结构 ├── config // Spring配置类,比如跨域、拦截器、文件上传配置 ├── security // 认证授权相关 ├── module │ ├── user // 用户与权限 │ ├── equipment // 器材分类、器材信息 │ ├── rental // 租赁订单、归还记录、报修单 │ └── dashboard // 统计分析 │ ├── controller │ ├── service │ └── mapper这种结构的核心逻辑是:同一个业务域里的代码,改动的频率和原因是高度相关的。比如器材状态调整会连带着影响租赁模块的查询结果,如果它们被拆到不同层,跨模块调用时你要在多个不相关的地方找关联点;按域分包后,在同一个顶层包下就能看到所有相关类,重构起来方便得多。
对于毕设答辩,这种组织方式也是加分项。老师问“器材状态变更影响哪些模块”,你直接按照业务域展开讲,逻辑层次会更加清晰,比对着单独的controller和service说“这里是CRUD”高明得多。
2. 核心功能模块拆解与数据库建模
2.1 物资体系与租赁流程的状态机设计
器材管理系统的核心不是“增删改查”,而是状态机。器材状态的变化直接驱动了整个系统的行为。
我把器材状态定义为:
| 状态 | 含义 | 触发动作 |
|---|---|---|
| AVAILABLE | 可租 | 管理员入库/归还验收合格 |
| RENTED | 已租出 | 用户下单且管理员核验出库 |
| REPAIRING | 维修中 | 用户报修或管理员主动送修 |
| SCRAPPED | 已报废 | 管理员评估无法修复 |
对应租赁订单的状态:
| 状态 | 含义 |
|---|---|
| PENDING | 待核验,用户提交申请但尚未取件 |
| ACTIVE | 租赁中,器材已出库,租期计时中 |
| RETURNED | 已归还,管理员完成验收 |
| OVERDUE | 超期未还 |
| CANCELLED | 已取消 |
这两个状态机是系统的“骨架”。所有复杂业务规则,比如超期罚金计算、押金退还、库存扣减时机,本质上都是状态变更时的副作用。设计数据库时,所有做状态流转的表都要留一个status字段,并且要保留操作类型和操作时间,方便追责和盘点。
我在自己做过的一版设计里,给器材表加了一个lock_version字段(乐观锁版本号),每次扣减库存或改变状态之前,先校验版本是否匹配,不匹配就说明有人并发操作了,直接返回“请刷新后重试”。在毕设答辩中,面试官特别容易问“多个用户同时租同一个最后一台器材怎么办”,有了这个设计,回答起来就是加分项。
2.2 数据库表结构设计:从用户到账单一口气画完
系统需要的核心表包括:
用户表sys_user
- 字段:id、username、password(BCrypt加密存储)、real_name、phone、role(用户/器材管理员/系统管理员)、status、created_time
器材分类表equipment_category
- 字段:id、category_name、description、sort_order
- 用途:将篮球、羽毛球拍、轮滑鞋、体操垫等器材按大类管理,后续做统计时以分类为维度。
器材信息表equipment
- 字段:id、category_id、equipment_name、equipment_code(唯一编号,用于扫码/条码)、specification、unit_price(单日租金)、deposit_amount(押金)、status、location(存放位置)、purchase_time、image_url、description
- 注意:不建议把“库存数量”直接设计成总数字段,而是一器一物一条记录。否则你有20个篮球,租金、报修、租期状态就无法落到具体的“那一个篮球”上。用唯一的
equipment_code管理每件实物,后面盘点、故障追溯都会清晰得多。
租赁订单表rental_order
- 字段:id、order_no(业务单号)、user_id、total_deposit、total_amount(预付租金)、rental_days、start_date、end_date、status、checkout_admin_id(出库核验员)、checkout_time、return_admin_id、return_time、actual_amount(结算金额)、fine_amount(罚金)、remark
租赁明细表rental_order_item
- 字段:id、order_id、equipment_id、equipment_name、unit_price、deposit、rental_days
- 用途:一个订单租多件器材时,每条明细记录对应一件具体器材的租赁情况。
报修记录表repair_record
- 字段:id、equipment_id、order_id(关联到是哪一次租赁后报修的)、user_id、repair_reason、images、status(待处理/维修中/已完成/已报废)、handler_id、handle_time、handle_note、cost
除以上外,还要准备一个操作日志表(谁在什么时间对什么数据做了什么操作),以及系统配置表(存放可调的参数,比如每日租金倍率、押金规则、超期罚金每24小时费率),这两个表虽然简单,但会让整个系统显得很完整。
数据库是我一直强调要优先做的环节。业务代码可以后续调整,表结构一旦确定就很难改。我在设计时额外的习惯是为每张业务表都加上create_time和update_time,用MyBatis Plus的自动填充功能处理,这样在历史记录和报表统计中几乎不需要额外写SQL就能定位数据。
2.3 租期、押金和罚金的计算逻辑
这里有一个很容易被忽略的点:租期计算必须细化到“自然日+小时”的粒度。
比如用户周一上午10点租一个羽毛球拍,租期“3天”,那归还截止时间是周四上午10点,而不是周四零点。超过时间就算超期,罚金按照一个固定的小时费率累计。我建议在设计时单独留一个常量配置fine_rate_per_hour,而不是在代码里写死数字,方便管理员按实际情况调整。
押金逻辑也需要提前想清楚:用户下订单时,前端可以先做一个押金的“预估展示”,但真正的冻结或扣款操作只有支付模块才会触发。如果做的是线上支付(微信/支付宝),需要使用押金+租金的合并金额去创建支付单;如果做的是线下交易,则押金状态标记为“线下已收”,由管理员确认。
归还时的结算公式为:
应退押金 = 原收取押金 - 维修扣款 - 清洗费用 - 超期罚金(如从押金扣除) 实际支付租金 = 预付租金(租期内)+ 超期罚金(超出部分)这个逻辑虽然不复杂,但如果不提前把规则整理成一份表格,编码时很容易把“应退金额”“需补缴金额”搞混。建议在需求阶段就用Excel把用例场景列出来,每个场景写清前期状态、操作、后期期望,把这条链路作为合同的输入项。
3. 后端SpringBoot实现的核心细节
3.1 工程搭建与依赖选型
SpringBoot版本我建议使用2.7.x,原因是这个系列生态成熟,无论是Spring Security还是第三方组件都有充足的案例参考。新版的SpringBoot 3.x虽然也好,但部分插件和教程还停留在旧版,遇到问题时排查成本高。
核心依赖通常包括:
spring-boot-starter-web:提供Web服务与JSON序列化spring-boot-starter-validation:参数校验mybatis-plus-boot-starter:数据访问层,省去大量基础Mapper代码mysql-connector-java:数据库驱动jjwt或java-jwt:生成和校验JWT令牌hutool:工具类库,处理日期、加密、字符串等,省时省力lombok:减少getter/setter样板代码spring-boot-starter-test:单元测试
使用MyBatis Plus主要看中它的分页插件和条件构造器。器材列表按分类筛选、按名称模糊搜索、租赁记录分页查询都是高频功能,手写XML虽然也能做,但开发效率差距很大。
3.2 统一返回结构与异常处理
前后端分离项目的第一要务是约定接口的数据格式。我在所有项目里都用统一的结构:
{ "code": 200, "message": "success", "data": {} }对应的Java通用类大致长这样:
public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }然后配合@RestControllerAdvice全局异常处理,捕获业务异常、参数校验异常、未知异常,统一转换到上面的结构。这套组合能省掉controller里大量重复的try/catch逻辑。
我记得有一次在排查线上问题时,由于当时没有加全局异常处理器,SQL报错信息直接以默认格式返回给了前端,前端弹窗里显示的居然是带SQL语句的完整堆栈,这不仅影响体验,还有信息泄露的风险。加上@RestControllerAdvice后,前端拿到的永远是{code:500, message:"服务异常"}这类安全结构,具体原因只记在服务端日志里。
3.3 认证与权限控制:拦截器 + JWT
因为是前后端分离项目,Session方案不太合适,更主流的做法是JWT鉴权。
基本流程:
- 用户登录时,后端校验用户名密码(BCrypt加密比对,不建议用MD5)。
- 校验通过后生成一个JWT令牌,里面包含用户ID、用户名、角色、过期时间。
- 用户后续请求在
Authorization请求头携带Bearer <token>。 - 后端用拦截器读取并校验Token,解析出用户身份,放入
ThreadLocal上下文。
我在项目里通常写一个AuthInterceptor,注册时排除登录、注册、器材列表等公开接口。对管理端接口,再用@RequireRole("ADMIN")这类自定义注解做细粒度控制,本质就是在拦截器里校验当前用户角色。
实际操作中有同学会想省事,把Token校验放在AOP里,但实际上拦截器是更合理的切入点:不进入Controller就能把非法请求挡掉,还能配合CORS预检请求处理,非常方便。
3.4 核心接口设计:租赁与归还的完整链路
这里说一下租赁下单和归还验收这两个核心接口的实现思路。
租赁下单接口/api/rental/create
入参大致包含用户ID、器材ID列表、租期天数、开始日期。
服务端要做这五件事:
- 校验用户状态是否正常,是否有未归还的欠费记录。
- 校验每件器材状态是否为
AVAILABLE。 - 计算租金总额与押金总额,把订单明细数据整理好。
- 插入订单主表,状态为
PENDING。 - 逐条插入订单明细表。
这里要注意:不能在下单阶段就把器材状态改成RENTED。因为用户可能下单之后不支付或者不来取件,如果此时锁定器材,会让真正的租客无器材可用。正确做法是:支付成功并且管理员“确认出库”时,才把器材状态改为RENTED。如果超过一定时间(比如24小时)还不出库,自动取消订单,释放器材。
管理员出库接口/api/rental/checkout
入参:orderId + 管理员操作人。
此处做的操作:
- 校验订单状态必须是
PENDING。 - 锁定订单(乐观锁或悲观锁均可,建议在数据库层面用行锁
SELECT ... FOR UPDATE更新状态)。 - 逐件更新器材状态为
RENTED。 - 记录出库时间、管理员ID。
归还验收接口/api/rental/return
归还不是一个POST就完事,我的习惯是拆成三步:
POST /api/rental/return/apply:用户提交归还申请,带归还器材的清单。POST /api/rental/return/check:管理员逐件核验器材状态(外观、可用性),如有损坏,关联报修单。POST /api/rental/return/confirm:管理员确认归还,计算费用,更新订单状态为RETURNED,更新器材状态为AVAILABLE或REPAIRING。
这个三步拆法在毕设答辩里也很值得讲:每一步都有明确的职责边界和操作记录,体现了系统设计的完整度。
3.5 并发控制与事务管理
并发是管理员系统最容易暴露问题的环节,也是最容易被初学者忽略的。
场景是这样的:网球场有12副网球拍,用户A和用户B同时发起租赁,都锁定了同一副球拍。如果我们只检查状态为AVAILABLE便直接创建订单,那两单都会创建成功,发放时就会撞车。
解决思路有几种,我按顺序推荐:
- 乐观锁版本号:在
equipment表加version字段,更新时用UPDATE equipment SET status='RENTED', version=version+1 WHERE id=? AND version=? AND status='AVAILABLE'。如果更新影响行数为0,说明已被别人抢先,需要给用户提示。 - 数据库行锁:在事务中先
SELECT ... FOR UPDATE锁定该器材记录,再做状态判断,能避免超卖。 - Redis分布式锁:适合多实例部署场景,但毕设项目基本是单实例,用前两种就够了。
我这里推荐用乐观锁,因为代码侵入少,不影响读取性能,也不需要额外的中间件。真正出问题时,提示用户“该器材刚被租走,请选择其他器材”,体验也说得过去。
4. 前端Vue实现与页面组织
4.1 工程创建与基础配置
前端我在正式项目中用的是Vue 3 + Vite + Element Plus,但很多同学的毕设环境可能已经接了Vue 2的课,那也完全没问题。下面按Vue 3来讲解,Vue 2的思路是一致的。
创建项目:
npm create vite@latest equipment-rental-web -- --template vue cd equipment-rental-web npm install npm install vue-router@4 pinia axios element-plus考虑到UI组件库对美观度的影响很大,Element Plus的表格、表单、弹窗组件能让你在很短时间内做出一套风格统一的后台界面。有条件的话,再加一个unplugin-auto-import和unplugin-vue-components插件,组件按需引入,安装后配置一下vite.config.ts,构建产物体积能小不少,首屏加载也会更快。
项目内的目录组织我通常是这样:
src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 公共组件 ├── layout // 后台布局(侧边栏+顶部栏) ├── router // 路由配置 ├── store // Pinia全局状态 ├── utils // 工具函数,比如请求拦截器 └── views ├── dashboard // 首页/统计 ├── equipment // 器材列表/分类管理 ├── rental // 订单确认/订单列表/我的租赁 ├── user // 登录/注册/个人中心 └── system // 用户管理/系统配置4.2 路由与权限菜单的动态生成
管理系统通常希望不同角色登录后看到不同的菜单。一种常见的设计是:前端路由分静态部分(登录、注册、公共页面)和动态部分(管理面板),动态部分根据当前用户的角色从后端获取菜单权限,再router.addRoute()动态注册。
后端可以提供一个接口:GET /api/user/menus,返回当前用户可见的菜单列表,结构类似:
[ { "path": "/rental/manage", "name": "RentalManage", "component": "rental/RentalManage.vue", "meta": {"title": "租赁管理", "icon": "List"} } ]在前端路由守卫里,每次跳转前检查store中是否已加载权限,如果没加载则先请求菜单再继续跳转。
这样实现的好处是:不同角色的前端代码包是一样的,但页面入口和按钮显隐会随权限不同而变化。管理端页面普通用户根本进不去——因为路由表里就没有注册对应路径。
4.3 请求封装与状态管理
前端请求建议统一封装一个Axios实例。核心点包括:
baseURL设置成后端服务根路径。- 请求拦截器里从
localStorage或Pinia里取Token,设置Authorization头。 - 响应拦截器里统一处理后端返回的
code:200正常;401则清空登录态并跳回登录页;其他code统一弹ElMessage.error()提示。
这样一个页面里调用接口就只需要关心业务数据,不用每个地方都写重复的错误提示逻辑。
全局状态管理我用Pinia,主要存两类数据:
userStore:当前用户信息、Token、权限菜单。appStore:侧边栏折叠状态、全局加载状态。
不建议把器材列表、订单列表这类业务数据放到全局Store里,因为它们的读频率低、且每个页面只需要局部数据,放全局反而会让组件间状态耦合变差,代码越来越难维护。
4.4 核心页面功能拆解
器材展示页:这是普通用户的入口,也是前端“门面”。我建议用卡片式布局展示器材封面图、名称、单日租金、押金、剩余数量。卡片底部放“立即租赁”或“加入租借单”。列表上方放分类筛选Tab和关键字搜索框,支持按“可租”“全部”做状态过滤。
下单页:类似购物车的“租借清单”页。用户从器材页添加器材到清单,在这个页面选择租期天数、开始日期,页面底部实时计算总金额与预计押金,再提交生成订单。金额计算可以用前端先演示,但最终以后端订单接口返回为准,避免前端篡改金额。
个人租赁中心:展示自己发起的订单,有状态标签、订单详情、操作按钮(取消订单/申请归还/查看详情)。页面上最好区分“当前待办”和“历史订单”两个Tab,空间布局更舒适,查询效率也更高。
管理后台:核心页面是器材管理表格、租赁订单审核表格、报修处理表格。表格组件开启列筛选、分页和排序;为了让管理员操作更高效,凡是需要操作的单行,都使用“操作列”放按钮,但不要全部展开,避免页面拥挤。关键操作(比如报废器材)建议弹出ElMessageBox.confirm二次确认,防止误操作。
仪表盘/统计页:用ECharts绘制几个统计图表,比如近7天租赁趋势折线图、器材分类租赁饼图、热门器材Top10柱状图。数据接口由后端提供聚合SQL,前端只负责渲染。这页不仅好看,也是答辩时展示系统价值的重点页面。
4.5 联调阶段的前端配合要点
联调最容易出的问题就是接口字段对接不一致。后端返回userId,前端接口定义写的是user_id;后端要求时间格式为yyyy-MM-dd HH:mm:ss,前端直接用Date对象传递,解析结果错位。这种事反复发生一次就要十分钟起步,非常影响效率。
我的建议是:
- 前端统一维护一份接口定义文件,比如
src/api/rental.js,每个接口注释清楚入参、出参结构。 - 后端用Swagger/OpenAPI生成接口文档,前端直接看
swagger-ui.html测试调用,保证字段名一致。 - 联调前双方先跑一遍核心场景:登录、查看器材、提交订单、管理员审核、归还结算。这个流程能跑通,剩下的小边界基本也不会差。
5. 实际开发中的高频问题与排查清单
5.1 跨域(CORS)问题
前后端分离开发时,最经典的问题就是跨域。前端跑在http://localhost:5173,后端跑在http://localhost:8080,浏览器默认会拦截跨域请求。
SpringBoot端的解决方法是配置一个全局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不能直接写*。另外,添加拦截器后,要保证OPTIONS预检请求不被拦,否则跨域配置写了也过不去。
我遇到过最隐蔽的跨域问题不是配置不对,而是后端自定义拦截器里对OPTIONS请求返回了401,导致浏览器整个链路直接断了。排查的时候脑子里全是“为什么配置了还是不行”,最后打开浏览器Network面板看到Preflight请求的状态码,一下子就暴露了。
5.2 日期与租期计算的时区坑
LocalDateTime使用时,前后端传输默认会序列化成数组格式,比如[2025, 5, 12, 8, 30, 0],非常不直观而且容易解析出错。我通常会加一个Jackson配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时在项目里统一使用LocalDateTime,不要混用Date和LocalDateTime,否则MVC参数绑定、Jackson序列化都会出现各种奇怪问题。早年在一次统计租金按日的查询中,因为日期格式不一致,前端传过来的是2025-06-01,后端存的是时间戳字符串,导致当天新增订单的租金统计漏了很多数据,后来统一成LocalDate做日期维度,问题立刻消失。
5.3 图片上传与静态资源映射
器材图片、报修图片,常见做法是上传到服务器本地目录,由Nginx反向代理访问。开发时SpringBoot需要加静态资源映射:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/uploads/**") .addResourceLocations("file:" + uploadDir); } }如果毕设是单机部署,也可以把图片存储路径配置到application.yml里的一个自定义字段,用@Value在代码里读取。
图片上传接口要注意限制文件大小、类型,建议前端用Element Plus的上传组件做前端压缩和校验,后端用MultipartFile接收后再校验。application.yml里记得加:
spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB不然文件稍大一点就会报MaxUploadSizeExceededException,而且这个异常如果不做全局捕获,默认会返回不友好的HTML错误页,严重影响体验。
5.4 分页查询中的“数据越翻越乱”
列表分页是用得最多的查询逻辑,但初学者经常踩一个坑:前端传pageNum和pageSize,后端用MyBatis Plus分页插件,却忘了写ORDER BY。没有排序的SQL在数据量变大后,翻页时会出现记录重复或漏掉的情况,因为数据库返回顺序不稳定。
所有分页查询,特别是订单列表和日志列表,必须显式加上排序条件,比如:
LambdaQueryWrapper<RentalOrder> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(RentalOrder::getUserId, userId) .orderByDesc(RentalOrder::getCreateTime);另一个分页相关的坑是“总数统计”。如果查询条件中包含多表join,还需要用select count(distinct 主键)的方式去重,否则总条数会膨胀。MyBatis Plus的分页插件对单表查询还算智能,但多表联查时最好手动指定count查询,避免统计不准。
5.5 部署时的端口、数据库连接和内存问题
毕设最后通常要演示部署,这里也有一些常见的坑。
后端打包成JAR之后,可以用java -jar运行,端口默认8080。如果演示环境端口被占用,用--server.port=9090指定一个新端口即可。
数据库连接那一套,最容易错的是MySQL时区配置。连接串上最好加:
jdbc:mysql://localhost:3306/equipment_rental?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false否则经常看到The server time zone value '�й���ʱ��' is unrecognized这种乱码报错。
还有一点容易被忽略:Vue前端打包后是纯静态文件,可以直接用nginx托管,把dist目录指向nginx根目录,同时配置一个反向代理转发/api的请求到后端8800端口。这样部署起来甚至不需要后端直接暴露端口,结构更规范:
location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }如果只是做演示,也可以用vite preview临时托管构建产物,但只适合开发阶段,不建议长期使用。
5.6 一些容易忽视的“体验负面”细节
很多功能从后端接口到前端交互都能跑通,但体验上还是欠打磨。以下是我在实际测试中反复被用户吐槽的点:
- 器材图片缺失:不设置默认占位图时,图片挂了会导致卡片布局错乱。
- 金额精度:后端Decimal类型返回前端,会变成字符串,前端排序和计算时要先
Number()转换。前后端浮点计算都有精度问题,涉及金额的字段,后端要用BigDecimal,数据库要存decimal(10,2),前端展示时用toFixed(2)。 - 空状态:列表没有任何数据时,要显示“暂无数据”,不然用户会误以为加载失败。
- 按钮重复点击:提交表单时,按钮要做
loading状态处理,否则用户连续点击,会创建多条一样的订单。这个在前端加锁,同时在后端接口做幂等校验(订单号唯一约束),双重保障。 - 邓白氏/扫码模块:如果毕设需要增加亮点,可以引入器材条码/二维码管理。生成二维码用
qrcode前端库,在后端给每件器材生成唯一编码,管理员用扫码枪或手机扫一扫直接进入验收流程,整套系统的完成度会立刻拉升。
6. 总结一下我个人的实操体会
这套系统我完整从零到尾走过好几遍,最大的体会是:它是“小而全”的典型项目,覆盖了常见业务系统的全部关键环节。你要处理的不是单纯CRUD,而是用户认证、资源状态机、并发控制、前后端联调、权限区分、上传下载、统计报表、部署排错。每一块单独拿出来都能在面试时聊出不少东西。
如果你现在准备动手,我给你最直接的三条建议:
第一,先画状态机,再画数据库,最后写代码。类似“器材”“订单”“报修”的状态流转图,画出来之后,整个系统的规则就清晰了,写代码时根本不需要边写边猜。即使后期老师追问需求细节,你也可以直接拿出状态图说明业务边界。
第二,后端先把接口跑通,前端可以做Mock并行开发。别等着后端完全完成才开始前端,接口定义好了,前端用Mock数据就能先做好页面和交互,两边最后一天联调是完全可以的。
第三,每一步都做一点体系化积累。比如把全局异常处理、统一返回结构、JWT拦截器、分页封装这些模块做成自己的“脚手架”,这次项目用完,下次无论是写课程设计还是给公司搭内部工具,都能直接复用。这些公共能力才是你真正从项目里沉淀下来的东西,而不是那份最终文档里的截图。
要问我后续还能在哪些方向继续增强,我最近在研究的是“租赁预估价实时计算加推送提醒”以及“基于器材利用率分析批量采购建议”这两个方向。前者需要写定时任务扫描订单到期时间,提前24小时给用户发短信或微信模板消息;后者则需要在现有统计SQL基础上加更多维度的汇总。如果你已经把这个系统做得足够稳,不妨往这两个方向试一试,难度不大,但会让整套系统的“智能感”上一个台阶。