1. 项目全貌:拆开标题看本质
1.1 这个项目到底在做什么
第一次看到这个题目,我脑子里冒出来的是:这不就是把共享单车换成共享车位吗?但真正把需求理顺之后发现,事情比想象中复杂。停车位不像单车那样扫码就能骑走,它有时段、有归属、有位置、有计费规则,还得处理预约了却没来、来了发现被占、超时离场、业主投诉这些现实问题。所以这个基于微信小程序的车位共享系统,本质上并不是一套普通的增删改查代码,而是一条从车位发布、审核、检索、预订、支付到履约结算的完整业务闭环。
从技术角度看,它就是一个典型的前后端分离全栈项目:后端用 Java 加 Spring Boot 提供接口,前端用微信小程序作为客户端,MySQL 存储业务数据,Redis 做缓存和并发控制。用户在小程序里注册登录后,可以把自家车位的空闲时段挂出来出租,也可以搜索附近车位按小时或按天预订;管理员在后台审核车位信息、处理订单纠纷、查看整个社区的停车数据。这个形态放在智慧社区这个场景下非常自然,也是毕业设计里比较讨巧的选题,因为它既有业务复杂度能写到论文里,又没有高到本科生做不完。
1.2 适合谁参考,难度如何评估
如果你正在找毕设题目,或者已经定了这个题目但不知道从哪里下手,那这篇文章就是按实际开发顺序来写的,从需求到技术选型、从建表到功能落地、从常见报错到答辩注意点,基本都能覆盖到。它的定位是一套可以抄作业但又能讲清楚原理的方案,不是贴一堆代码让你自己猜。
难度评估我按个人经验给个量级:中等偏上一点点。对比纯图书管理、学生选课这类管理系统,它多出来的难点主要在三块:一是微信登录和支付回调这套外部对接逻辑;二是同一车位同一时段的并发抢单怎么避免超卖;三是小程序端有体积上限和真机调试这两道坎。如果前期把这三个点提前准备好,后面开发其实很顺畅。整个项目正常周期大概五到七周,其中数据库设计一旦定下来就不要再轻易动,否则返工成本很高。
2. 需求先想清楚,开发才不返工
2.1 角色怎么划分才合理
车位共享系统最忌讳一开始就把角色拆成"业主"和"租客"两个完全隔离的身份,因为真实场景里一个人可能既有车位要出租,也想偶尔租别人车位。我在设计时把用户统一成平台的注册用户,然后通过"绑定关系"来区分他的行为:他登记了自己的车位,就是这条车位的发布者;他去下单租别人的车位,就是这条订单的承租方。管理员则单独一个后台身份,不参与小程序端的交易。
所以角色其实是两类:普通用户和管理员。普通用户在小程序端能发布车位、管理自己车位、搜索预订、发起评价投诉;管理员在管理后台能审核车位、处理用户申诉、查看订单流水和基础统计。这个划分能让数据模型简单很多,后续加功能也不容易把逻辑绕晕。
2.2 一条订单从产生到结束要经过哪些状态
搞懂订单状态流转比写代码更重要。我按实际履约过程把订单拆成了这样:用户选好车位和时段后先创建订单,此时状态是待支付;支付成功后变成已支付待入场;到场扫码或输入验证码开始停车,变成进行中;离场后进入待结算;双方无争议自动结算为已完成;如果中途有一方取消,就是已取消;涉及退款的,还有退款中、退款完成。
这个状态机决定了数据库字段和接口行为。比如待支付订单超过十五分钟没付款,系统要定时取消并释放车位;已支付但车主想取消,要走退款流程;进行中订单如果承租方超时未离场,后端要支持管理员介入延长时间或者强制结束。建议你把状态枚举单独定义一个类,不要散落在各处写魔法数字,否则后期改状态逻辑时哭都来不及。
2.3 完整功能清单
我把自己最终交付的功能列成了一张表,方便照着做模块划分:
| 模块 | 功能点 | 说明 |
|---|---|---|
| 用户中心 | 微信登录、个人资料、我的车位 | 通过 openid 唯一识别 |
| 车位共享 | 发布车位、编辑时段、设置价格、车位状态管理 | 新发布需后台审核 |
| 检索预订 | 附近车位列表、关键词搜索、价格排序、时段筛选 | 调用地图定位周边 |
| 订单交易 | 创建订单、取消订单、支付、退款、时段冲突校验 | 核心模块 |
| 履约控制 | 验证码入场、离场确认、超时提醒 | 结合订单状态机 |
| 评价投诉 | 订单评价、投诉工单、协商记录 | 管理员介入处理 |
| 管理后台 | 车位审核、用户管理、订单查询、退款处理、数据统计 | 独立管理端页面 |
除了这些,我还加了消息通知,比如预约成功、入场提醒、订单结算完成都会推送模板消息。这部分是加分项,但如果时间紧张,放到最后再做也完全可行。
3. 技术选型:为什么是这套组合
3.1 后端为什么选 Spring Boot 2.7 加 MyBatis Plus
后端选型上,Spring Boot 是当前 Java 后端绝对的主流,毕设用它不会出错。版本我建议用 2.7.x 而不是最新的 3.x,不是因为 3.0 不好,而是 3.x 强制要求 JDK17,部分老教程和老依赖在 3.x 下会有兼容调整,对毕设来说容易徒增烦恼。如果你本机正好装了 JDK17 且想顺便学学新特性,那用 3.2 也可以,但接口写法差异不大。
持久层选 MyBatis Plus,理由是它能省掉大量单表 CRUD 的重复代码,内置的分页插件也方便做列表翻页。复杂查询比如车位时段冲突校验,手写 SQL 也不难。有人喜欢 JPA,但 JPA 在处理这种带状态字段、多条件组合查询的业务时,很多同学反而会被对象关系映射绕晕。MyBatis Plus 更简单直白,面试时也好解释 SQL 和 ORM 的关系。
其余组件按需选择:Redis 用来缓存热点车位数据和做分布式锁,同时帮登录 token 做过期管理;JWT 负责无状态鉴权;Knife4j 可以自动生成接口文档,写论文和答辩演示时直接打开一个地址就能看到所有接口,比截图 Postman 省事。Lombok 减少 getter setter 模板代码,Hibernate Validator 做参数校验,都是老生常谈但确实好用的东西。
3.2 小程序端原生还是 uni-app
小程序端有两种主流做法:微信原生小程序和 uni-app 跨端框架。毕设我更推荐原生小程序,原因很现实:原生小程序用微信开发者工具直接调试,报错信息直观,教学资料也最丰富;uni-app 虽然能后续编译到 App 和 H5,但增加了框架概念、条件编译、打包配置这些额外学习成本,反而容易把项目拖垮。
如果你的选题描述里明确写了"微信小程序",那答辩老师大概率也只关心小程序端效果,原生开发完全够用。页面划分上,控制好 tabBar 和普通页面的关系,首页做车位列表、发现页做地图搜索、个人页做我的车位和订单,逻辑分得开,代码也不会乱。小程序端有一个细节容易被忽略:自定义顶部导航栏时,要用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置来算导航栏高度,不然在 iPhone 和安卓上状态栏高度不一样,页面头部会顶到屏幕最上面。
3.3 架构与部署怎么安排
整体架构保持单体应用最合适。客户端是微信小程序,服务端是 Spring Boot 单体,数据层是 MySQL 加 Redis,文件上传先存本地目录,答辩前再决定要不要接对象存储。不要为了显得厉害去拆微服务、引入消息队列,对毕设来说这是过度设计,部署和演示都会变成坑。
本地开发时,小程序端在开发者工具里可以勾选"不校验合法域名"来直接请求http://localhost:8080,省去折腾域名证书的时间。但要注意,真机预览时手机访问不了你电脑的 localhost,需要让后端监听局域网 IP,并把小程序里的 baseUrl 改成局域网地址,比如http://192.168.x.x:8080,手机和电脑连同一个 Wi-Fi 才能调通。这个细节我第一次演示时就踩过,现场慌了半天。
4. 数据库设计:表结构怎么定才不返工
4.1 聊聊核心表
数据库是整个项目的底盘,表一旦建错,后面每个接口都在别扭。我最终保留了八张核心表:用户表、车位表、订单表、计费规则表、评价表、投诉表、消息表和后台管理员表。
用户表除了微信 openid、昵称、头像这些基础字段,还要有手机号、账户状态和创建时间。车位表是最容易设计错的表,一开始我只放了所属用户、位置、状态,后来才发现必须把出租的时间段和价格拆出去单独设计,因为一个车位可能同时有"白天出租""晚上出租"不同价格。所以我增加了计费规则表,一条规则对应一个车位在一个时段段的价格,按小时计费还是按次计费也能区分。
订单表不用多说,是整个系统的核心。评价表和投诉表都通过订单号关联回原始订单,保证可追溯。这里我建议所有业务表统一加上create_time和update_time,并且用datetime类型全库一致,避免后面排序和统计时摸不着头脑。
4.2 订单表的设计要点
订单表我给出一个比较接近实际生产的建表结构,你可以直接用它当蓝本:
CREATE TABLE `order_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `space_id` bigint(20) NOT NULL COMMENT '车位ID', `renter_user_id` bigint(20) NOT NULL COMMENT '承租方用户ID', `owner_user_id` bigint(20) NOT NULL COMMENT '出租方用户ID', `plan_start_time` datetime NOT NULL COMMENT '计划开始时间', `plan_end_time` datetime NOT NULL COMMENT '计划结束时间', `actual_start_time` datetime DEFAULT NULL COMMENT '实际入场时间', `actual_end_time` datetime DEFAULT NULL COMMENT '实际离场时间', `order_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态', `pay_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '实付金额', `refund_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '退款金额', `cancel_reason` varchar(255) DEFAULT NULL COMMENT '取消原因', `version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_space_time` (`space_id`, `plan_start_time`, `plan_end_time`), KEY `idx_renter` (`renter_user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';这里有几个设计取舍。订单号一定要唯一,我生成规则是yyyyMMddHHmmss + 用户ID后四位 + 随机四位,保证演示时不重复就行。我没有在space_id、plan_start_time、plan_end_time上建联合唯一约束,因为取消和退款场景下同一时段可能合法地重新下单,硬约束会挡住正常业务;并发冲突的控制放在服务层处理,而不是靠数据库唯一键硬堵。
4.3 状态流转与索引设计
订单状态我建议用 tinyint 存数字而不是字符串,并且建一个状态枚举类做映射。比如 0 待支付、1 已支付待入场、2 进行中、3 待结算、4 已完成、5 已取消、6 退款中、7 退款完成。这样存储体积小、查询效率高,代码里用枚举名调用也看得懂。不要直接用中文存状态,不然统计时group by全是一堆中文,性能差不说还容易出乱子。
索引设计方面,用户经常按自己的视角查订单,承租方查renter_user_id、出租方查owner_user_id,所以这两个字段的单列索引比一个冗余的联合索引更灵活。车位时段查询是热点操作,(space_id, plan_start_time, plan_end_time)联合索引能让列表页和冲突校验都受益。订单号唯一索引就不用说了,支付回调和查询都靠它定位订单。记住一个原则:索引不是越多越好,优先保证 where 条件和排序字段能命中,那些不会用来查询的字段别乱加索引。
5. 核心功能实现与踩坑记录
5.1 微信登录不是直接存 openid 那么简单
微信登录的流程大家应该都背过:小程序端wx.login()拿到临时 code,传给后端;后端拿 code 调微信接口换 openid 和 session_key;再用 openid 去用户表找用户,找到就返回登录成功,找不到就自动注册一个新用户。我习惯在换到 openid 之后,立刻生成一个自己的 token,比如用 JWT 把 userId 和开放平台 openid 装进去,而不是把微信的 session_key 直接暴露给前端。
这么做有三个好处。一是 session_key 是微信侧的身份凭证,本应该只在后端使用,一旦下发到前端就有被滥用风险;二是 JWT 可以自己控制过期时间,比如七天免登录,用户在小程序里不用频繁重新授权;三是后面要做管理员接口和小程序端接口统一鉴权,在 Spring Boot 里加一个过滤器校验 JWT 就行了,非常统一。注意一个小坑:微信登录换 openid 的请求返回格式不固定,接口偶发失败时不能直接拿返回值,要先判断返回码,再判断 openid 是否为空,否则会把空 openid 存进用户表。
5.2 车位发布与地图定位
车位发布表单里最麻烦的是位置信息。我用的方案是在小程序端接入腾讯地图插件,让用户在地图上选择一个经纬度,再反向解析出小区名称和具体地址,提交到后端只存经纬度加地址文本。千万不要让用户手动输入一个字符串地址,否则你后面做"附近车位"搜索时,根本没法计算距离。
经纬度存储用 decimal 类型,保留六位小数就够了。附近搜索的排序逻辑是先按经纬度算出与当前用户的直线距离,再按价格和发布时间排序。演示时效果很直观,用户在小程序首页一点"附近车位",列表立刻按从近到远排好,这比单纯的列表分页有说服力。管理端审核车位图片时,建议强制至少上传一张实景图,避免用户发布虚假车位。
5.3 并发抢单的锁该怎么处理
车位共享最核心的难点,是同一个车位在重叠时段被两个人同时下单。这个问题我在前期反复推演过,代码层面如果不处理,等到答辩现场演示两台设备抢单翻车就晚了。我的方案是三层控制。第一层用 Redis 分布式锁锁住"车位加时段"这个 key,比如park:space:lock:{spaceId}:{start}:{end},拿到锁的人才允许往下创建订单;第二层在数据库层面用SELECT ... FOR UPDATE锁住车位行,防止同一个车位上的状态更新互相覆盖;第三层在事务里做重叠时段校验,查一下已支付和进行中的订单是否覆盖了这个时段。
核心代码大概长这样:
@Transactional(isolation = Isolation.READ_COMMITTED) public OrderResult bookSpace(Long spaceId, Long renterId, LocalDateTime start, LocalDateTime end) { ParkingSpace space = parkingSpaceMapper.selectByIdForUpdate(spaceId); if (space == null || space.getSpaceStatus() != SpaceStatusEnum.AVAILABLE.getCode()) { return OrderResult.fail("车位不存在或不可预订"); } int count = orderInfoMapper.countConflict(spaceId, start, end, OrderStatusEnum.WAIT_PAY.getCode(), OrderStatusEnum.PAID.getCode(), OrderStatusEnum.USING.getCode()); if (count > 0) { return OrderResult.fail("该时段已被预订"); } return createOrderAndUpdateSpaceStatus(space, renterId, start, end); }这里我特别用了READ_COMMITTED隔离级别,原因是 MySQL 默认的REPEATABLE READ在同一个事务里,后执行的普通查询可能看不到其他事务刚提交的数据,导致冲突校验漏判。用READ_COMMITTED后,每次查询都能读到最新已提交数据,对订单场景足够可靠。毕设答辩时如果能把这个点讲清楚,老师会认为你是真做过并发处理的,而不是只会背八股。
5.4 微信支付与回调幂等
微信支付接入是一个相对独立的环节。后端先调统一下单接口拿到 prepay_id,再组装参数返回给小程序端调起支付。支付成功后微信会异步回调后端接口,后端要在回调里做验签、校验金额、更新订单状态。这里的关键是回调可能重复通知,后端必须做幂等处理:先根据订单号查订单,如果已经是已支付状态,直接返回成功,不再重复处理。
毕设有个现实问题:个人主体小程序没有支付权限,很多同学申请不到商户号。我的建议是在系统里同时做一套"模拟支付",开发阶段走模拟通道,答辩时点一下"模拟支付"就当作支付成功,同时把微信支付的对接代码保留在工程里,论文里写清楚正式环境怎么切换。这样既不影响演示效果,又能体现你理解真实支付流程。模拟支付不代表偷懒,而是把不可控的外部环境对毕设的影响降到最低。
5.5 小程序列表加载更多和分包体积
列表加载更多几乎每个小程序都要做,网上的代码模板很多,但真正要跑通有几个细节。我在车位列表页用的是页面的onReachBottom,触底时把当前页码加一再请求一页,等后端返回的数据条数小于请求条数时,就把hasMore置为 false,显示"没有更多了"。同时把"首次加载"和"加载更多"分开处理,避免触底事件重复触发时连续发出好多个请求。
Page({ data: { list: [], pageNum: 1, pageSize: 10, hasMore: true, loading: false, }, onReachBottom() { this.loadMore(); }, loadMore() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); wx.request({ url: 'https://your-api/api/space/list', data: { pageNum: this.data.pageNum, pageSize: this.data.pageSize }, success: (res) => { const newList = this.data.list.concat(res.data.rows); this.setData({ list: newList, hasMore: res.data.totalPage > this.data.pageNum, pageNum: this.data.pageNum + 1, }); }, complete: () => this.setData({ loading: false }), }); }, });小程序体积超限也是热点问题,打包报错提示 source size 超过 2MB。我当时的做法是三件事并行:把上传的照片压缩并尽可能改到 CDN 引用;检查页面里有没有不小心引进了未使用的第三方库,尤其是地图库和图表库,能按需引入就按需引入;最后把订单确认页、个人中心这类低频页面拆到分包里,主包只保留 tabBar 页面和公共组件。这之后主包直接降到了 1.2MB 左右,运行起来也明显更流畅。
6. 常见报错排查与避坑清单
6.1 高频报错速查表
把我在这个项目里遇到的高频问题整理成一张表,每一条都是实际踩过的,不是网上复制来的。
| 报错或现象 | 可能原因 | 处理办法 |
|---|---|---|
| 接口返回 10002 | appId 或请求签名信息不一致 | 检查开发者工具里的 appid 和后端配置是否一致,查看具体返回信息定位 |
| 打包提示 source size 超过 2MB | 主包体积超限 | 分包加载、压缩图片、移除未使用依赖 |
| onReachBottom 不触发 | 页面内容高度不足或用了 scroll-view | 用页面滚动而不是 scroll-view,或在 scroll-view 上监听下拉到底 |
| 真机预览请求不成功 | 访问的是本机 localhost | 后端监听局域网地址、小程序 baseUrl 改局域网 IP、手机电脑同一网络 |
| 时间显示差八小时 | 时区没处理 | 数据库连接加 serverTimezone,后端统一 LocalDateTime,传输时明确格式 |
| Redis 缓存穿透 | 热点车位不存在时大量查询落库 | 缓存空结果加短过期时间,或接口做限流保护 |
| 重复支付回调 | 微信回调重试 | 回调里先查订单状态,已支付直接返回,保证幂等 |
| 数据库连接池满 | 慢查询或连接没释放 | 检查慢日志、给高频查询补索引、合理配置连接池大小 |
这里有一个使用开发者工具的小习惯:排查请求问题不要只盯着后端控制台,小程序端的 Network 面板能看到发起时间、请求参数和响应报文,很多前后端对不上的问题一眼就能看出来。我调试登录和支付回调时基本全靠它,比盲目猜代码高效得多。
6.2 几条实测出来的开发习惯
分享几个我自己总结的开发习惯。第一,所有状态字段在数据库里用数字,在代码里用枚举,前后端传递时用数字,但前端页面上要有一份状态对应中文文案的映射表,避免用户看到一串数字。第二,支付回调日志一定要打全,把请求体、验签结果、订单号都记下来,出了问题才有依据排查。第三,别急着把车位状态改成"已租出",订单取消后要能自动释放车位,这个逻辑写一个定时任务扫描超时未支付订单就够了。
另外在开发阶段,Redis 的 key 命名尽量带上业务前缀,比如park:user:token、park:space:detail、park:space:lock,这样在 Redis 管理工具里看时一目了然,排查过期和数据覆盖问题会轻松很多。这些小习惯不会直接影响功能是否能跑通,但在答辩演示和后续维护时,带来的好处是实打实的。
7. 这个项目做完之后的个人体会
整趟做下来,我最大的体会是:毕设项目不在于功能列表有多长,而在于核心链路能不能闭环。车位共享系统的亮点不在"能注册、能发布、能下单",而在订单从创建到结算的每一个状态转换是否经得起追问。我把数据库设计定下来之后,紧接着就写并发抢单那段,因为那才是这个题目真正区别于普通管理系统的地方。答辩时老师问的也正是这个点,你怎么防止同一车位被重复预约,讲清楚这个,整个项目的含金量就上来了。
最后再分享一个小技巧。做演示前准备两个微信号,一个当车主发布车位,一个当租客搜索下单,中间穿插一个管理员审核的动作,整个过程一气呵成,比边演示边现场敲代码要靠谱得多。手动输入经纬度、临时造测试数据这种事,一定会让你在老师面前手忙脚乱,提前用测试脚本填充好数据才是正经事。如果之后时间充裕,还可以在这个基础上加一个社区停车热力统计页,把订单数据做成图表,这会让你论文的"系统亮点"部分又多一张能打的图。