简介:基于微信小程序与SSM框架的共享停车位系统,面向毕业设计、课程设计及小程序后端开发学习者,完整呈现从车位发布、车辆管理到订单计费的一体化流程。资源共391个文件,压缩包约23.06MB,涵盖java/jsp后端源码、xml配置、class编译文件、jar依赖库,以及微信小程序端的js、wxml、wxss、json页面文件,并含png图标与数据库SQL脚本,目录结构清晰,便于按模块对照学习。系统主要实现了共享车位的创建与管理、车辆信息维护、附近车位定位显示、车位编号/位置/状态查看,以及订单记录、计费与结束确认等核心功能;个人中心模块还支持订单记录与反馈意见管理。对于需要快速搭建类似项目的开发者,可通过源码与数据库文件直接部署运行,重点学习SSM框架与小程序交互、订单计费逻辑及前后端数据联调思路。目前已有822人学习下载,适合作为毕业设计参考或实训项目二次开发。
1. 共享停车位小程序:一套SSM后端加微信小程序,能解决什么
共享停车位小程序这套方案,本质是把小区、写字楼里闲置的私家车位,通过微信小程序撮合给临时需要停车的人。标题里的SSM_car.rar是典型源码包形态:小程序前端、Spring+SpringMVC+MyBatis后端、SQL数据库脚本三件套。它的价值在于场景精准——熟人社区、单位园区这类小范围内,车位主发布空闲时段,车主按距离和价格预约,两端信息不再靠微信群吼。适合谁?做课程设计、毕业设计的学生,以及想用一个周末验证停车撮合需求的开发者。技术选型也务实:单机SSM加一台MySQL足够支撑几百用户量级,不需要引入复杂的微服务和中间件,先把业务闭环跑通再说。
2. 数据库设计先行:三张核心表撑起整个车位撮合业务
拿到这类源码包,我一般不会先看代码,而是先解压找SQL脚本,把表结构过一遍。原因很简单:SSM项目的业务边界基本由表定义决定,表字段设错了,后面Controller和Mapper全要跟着返工。共享停车位的核心业务可以压缩成三个实体:谁发布的、车位的物理信息、谁在什么时间用了它。落到表上就是用户表、车位表、订单表。
2.1 用户表与车位表:字段设计、索引与状态位
用户表不建议做得太复杂。微信小程序端通过wx.login拿到code,后端再调用官方接口换取openid,所以openid是天然的唯一登录凭证,直接建唯一索引。昵称、手机号属于展示和联系字段,余额字段是给模拟支付用的,如果订单直接走微信支付,余额字段可以不要,但课程设计阶段保留它更方便演示支付闭环。
车位表是这套系统的信息核心。经纬度用DECIMAL(10,6)存储,精度在小程序地图上足够用了;每小时价格用DECIMAL(8,2),避免浮点误差。这里有一个实战上的取舍:可用时段我用VARCHAR存"08:00"和"20:00",而不是用TIME类型或时间戳。原因是小程序端展示、表单回填都更省事,数据库查询也直观;代价是无法做复杂的跨天时段计算,但共享停车位场景里"每日固定可用时段"的模型够用了。
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,登录唯一凭证', `nickname` varchar(64) DEFAULT '' COMMENT '昵称', `phone` varchar(20) DEFAULT '' COMMENT '手机号,联系方式', `balance` decimal(8,2) DEFAULT '0.00' COMMENT '账户余额,模拟支付用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `t_parking_space` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '发布者id,关联t_user', `title` varchar(100) DEFAULT '' COMMENT '车位标题,如B2-018', `address` varchar(200) DEFAULT '' COMMENT '地址描述', `longitude` decimal(10,6) NOT NULL COMMENT '经度', `latitude` decimal(10,6) NOT NULL COMMENT '纬度', `price` decimal(8,2) NOT NULL COMMENT '每小时价格,单位元', `start_time` varchar(16) DEFAULT '08:00' COMMENT '每日可用开始时间', `end_time` varchar(16) DEFAULT '20:00' COMMENT '每日可用结束时间', `status` tinyint(4) DEFAULT '1' COMMENT '1可预约 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_geo` (`longitude`, `latitude`), KEY `idx_user` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;字段说明里最有讲究的是车位表的状态位。我见过有人把车位状态做成"空闲/占用/下架"三个值,但下单场景下这个状态很难维护:A用户占用中,B用户想预约明天下午,这时候车位是空闲还是占用?所以更合理的做法是车位表只保留"可预约/下架"两个状态,具体某个时间段是否被占,交给订单表去算。这一点是后面防超卖设计的前提,很多新手在这里第一节课就交学费。
索引方面,idx_geo覆盖经纬度范围查询,idx_user用于查询"我发布的车位"列表。两表都用了utf8mb4字符集,因为用户昵称里可能带Emoji,用utf8会报错,这是文本框直接输入的坑。
2.2 订单表与状态机:时间冲突检测是防超卖的前提
订单表是整套系统里最需要花心思的一张表。除了常规的订单号、用户、车位关联外,有两个字段容易被忽略:owner_id和start_time。owner_id冗余车位主的id,是为了结算和通知方便,不用每次关联查询车位表再查用户表;start_time和end_time用VARCHAR存"2025-03-20 14:00"这种格式,一方面和用户输入天然对齐,另一方面做时间冲突检测时可以直接用字符串比较,字典序和时间序在这套格式下是一致的。
CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号,展示给用户', `user_id` int(11) NOT NULL COMMENT '车主id,下单人', `space_id` int(11) NOT NULL COMMENT '车位id', `owner_id` int(11) NOT NULL COMMENT '车位主id,冗余字段,结算用', `start_time` varchar(16) NOT NULL COMMENT '预约开始,如2025-03-20 14:00', `end_time` varchar(16) NOT NULL COMMENT '预约结束,如2025-03-20 16:00', `amount` decimal(8,2) NOT NULL COMMENT '订单金额', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0待支付 1已预约 2进行中 3已完成 4已取消', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`), KEY `idx_space_time` (`space_id`, `start_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;订单状态机是这里的设计核心。0到4五个状态,流转路径是:待支付→已预约→进行中→已完成,待支付和已预约都可以取消到已取消。这套状态机撑起了整个业务流程,前端按钮文案、后端接口校验、数据库统计都依赖它。我习惯把状态定义写在代码注释和SQL注释双份,因为后面写MyBatis的update语句时,往往会偷偷加上WHERE status = #{expectStatus}来防止状态乱跳。
idx_space_time联合索引很关键。时间冲突检测的执行频率最高——每次下单都要查"这个车位在目标时段内有没有交叉订单",有了这个联合索引,查询走索引,在几百单的数据量下压力很小。冲突检测的SQL逻辑是:查同车位、状态在有效范围内、且时间段与目标时段有交集的订单数,NOT (end_time <= #{start} OR start_time >= #{end})这个条件能覆盖完全不相交的场景,剩下的就是有冲突。
3. SSM后端接口实现:从微信登录到订单闭环的完整链路
后端部分是典型的SSM三层结构:Controller接收小程序请求,Service处理业务规则,Mapper通过MyBatis操作数据库。这个项目不需要多复杂的架构,但有一个原则必须守住——Controller只做参数接收和结果返回,所有校验、状态流转、并发控制都放在Service层。否则业务逻辑一旦散落在Controller里,后面查订单状态异常时根本无从下手。
3.1 微信登录与token鉴权:code2session的调用细节
微信小程序的登录流程和传统账号密码不同。前端wx.login()拿到一个一次性code,后端拿这个code去微信官方接口换openid和session_key。这里有几个细节必须处理对:第一,code只能用一次,重复调用会被微信拒绝;第二,请求需要带上appid和secret,这两个配置项放在后端配置文件里,绝对不能写进小程序前端;第三,openid换到后不要直接当token用,而是生成一个自定义token返回给前端,后续请求都带这个token。
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { String code = dto.getCode(); // 1. 调用微信 jscode2session 接口,换 openid String openid = wechatService.code2Session(code); // 2. 查不到就自动注册,查到了直接返回 User user = userService.findOrCreate(openid); // 3. 生成登录态 token 并返回 String token = userService.createToken(user.getId()); return Result.ok(token); } }createToken的常见做法是UUID加用户id拼接后存Redis,设置7天有效期,小程序端每次请求在Header里带Authorization: token,后端用拦截器统一校验。如果项目里没引Redis,简单点可以查一张登录态表,但要注意清理过期数据。这里不建议用JWT——没有状态管理,用户封禁、踢人下线的需求不好做。
登录态校验拦截器只需要做一件事:从Header取token,查到有效记录就放行并往ThreadLocal里塞userId,Controller里就能直接拿到当前登录人。这套机制在多个接口复用,避免了每个方法都写一遍"拿用户"的重复代码。
3.2 车位发布与附近检索:MyBatis动态SQL的查询组合
车位发布接口的逻辑相对简单:基本信息校验通过后,插入t_parking_space表。真正有技术含量的是检索接口。微信小程序端传过来的搜索条件可能是关键词、价格上限、经纬度范围的任意组合,用MyBatis的动态SQL处理这类多条件查询是SSM项目的标准姿势。
<select id="searchSpaces" resultType="com.demo.entity.ParkingSpace"> SELECT * FROM t_parking_space <where> <if test="keyword != null and keyword != ''"> AND (title LIKE CONCAT('%', #{keyword}, '%') OR address LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> <if test="minLng != null and maxLng != null"> AND longitude BETWEEN #{minLng} AND #{maxLng} </if> <if test="minLat != null and maxLat != null"> AND latitude BETWEEN #{minLat} AND #{maxLat} </if> AND status = 1 </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select>这段SQL里有几个细节值得说明。<是XML里小于号转义,直接写<会导致XML解析报错,这是MyBatis新手最常见的报错之一。经纬度范围查询不是精准的距离排序,但足够实现"附近车位"的效果——小程序端拿到用户当前位置后,按一个矩形范围把经纬度上下限算出来传给后端。如果项目要求按距离从近到远排序,就需要在SQL里实时算Haversine距离,数据量大时性能和代码复杂度都会上来,小项目没必要上这个强度。
分页参数offset和pageSize由前端传入,默认offset=0、pageSize=10。排序用create_time DESC把新发布的车位置顶,这是最简单也最不容易出错的策略。关键词检索走LIKE模糊匹配,因为车位标题和地址本身短,数据量也小,不需要上全文索引。
3.3 下单核心逻辑:先锁车位再建订单
下单接口是这套系统里最容易翻车的地方。业务规则有四条:车位必须存在且可预约;预约时间必须合法,结束晚于开始;目标时段不能和其他订单冲突;同一时刻不能有两个人同时下单抢同一个车位。前三条是纯业务校验,第四条是并发控制,两者不能混在一起处理。
public Order createOrder(CreateOrderDTO dto) { // 1. 校验车位存在且可预约 ParkingSpace space = spaceMapper.selectById(dto.getSpaceId()); if (space == null || space.getStatus() != 1) { throw new BizException("车位不存在或已下架"); } // 2. 校验时间合法性 if (dto.getStartTime().compareTo(dto.getEndTime()) >= 0) { throw new BizException("结束时间必须晚于开始时间"); } // 3. 校验目标时段是否被占用 int conflict = orderMapper.countConflict( space.getId(), dto.getStartTime(), dto.getEndTime()); if (conflict > 0) { throw new BizException("该时段已被预约,换一个时间试试"); } // 4. 乐观锁:用条件update抢占车位 int rows = spaceMapper.lockById(space.getId()); if (rows == 0) { throw new BizException("手慢了,车位刚被预约"); } // 5. 生成订单并落库 Order order = buildOrder(dto, space); orderMapper.insert(order); return order; }核心在第四步。lockById执行的SQL是UPDATE t_parking_space SET status = 0 WHERE id = #{id} AND status = 1,影响行数为0就说明车位状态变了,直接拒绝。这一步既完成了并发控制,又把车位状态置为不可预约,一举两得。但要注意一个边界:如果订单随后取消,必须把车位状态改回1,否则这个车位的状态就脏了。
时间冲突检测的SQL依赖t_order表里的idx_space_time索引和字符串比较:
<select id="countConflict" resultType="int"> SELECT COUNT(*) FROM t_order WHERE space_id = #{spaceId} AND status IN (0, 1, 2) AND NOT (end_time <= #{startTime} OR start_time >= #{endTime}) </select>状态过滤用了IN (0,1,2),把待支付、已预约、进行中三种状态都视为占用时段;已完成和已取消的订单不参与冲突检测。这里的教训是:冲突检测必须和状态机绑定,如果只查未取消的订单,就会漏掉"待支付订单也占着时段"的情况。
4. 微信小程序端实现:页面结构、请求封装与订单状态流转
小程序端是用户直接接触的一层,核心目标是把后端接口的能力包装成顺手的界面。拿到源码后,先看目录结构是否清晰,再看请求封装是否统一,这两个点决定了后续改代码的效率。我见过不少次拿到的小程序端把wx.request散落在每个页面里,改一个baseUrl要翻十几个文件,这种结构建议直接重构。
4.1 小程序目录结构与request封装:换baseUrl就能对接
标准的小程序端目录应该是:pages放页面,utils放请求封装,components放可复用组件,app.js里做全局初始化。共享停车位项目最核心的就是utils/request.js——所有接口调用都走这一个文件,好处是统一带登录态、统一处理错误码、统一弹提示。
// utils/request.js const BASE_URL = 'http://localhost:8080/api'; function request(path, method, data) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + path, method: method || 'GET', data: data || {}, header: { 'Authorization': token }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail: (err) => { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这里约定后端统一返回{ code: 0, data: ..., msg: ... }结构,code非0视为业务失败。前后端约定好这一层,页面代码就不需要每个接口都写错误处理。BASE_URL在本地调试时是http://localhost:8080,真机预览要改成电脑的局域网IP,上线要换成HTTPS域名——这是小程序开发的固定流程。
4.2 发布与预约页面:表单校验、定位与时段选择
发布车位页面要解决三个问题:地理位置怎么拿、表单怎么校验、时段怎么选。地理位置用wx.getLocation获取,但前提是用户在app.json里声明了requiredPrivateInfos且调用前确认授权。用户拒绝授权后要有兜底——提示去设置页打开定位,否则发布不了车位。
发布表单的核心代码要注意前后端字段名必须一致:
Page({ data: { title: '', address: '', price: '', longitude: 0, latitude: 0, startTime: '08:00', endTime: '20:00' }, onPublish() { const { title, address, price, longitude, latitude, startTime, endTime } = this.data; if (!title || !address || !price) { wx.showToast({ title: '请填写车位标题、地址和价格', icon: 'none' }); return; } if (isNaN(Number(price)) || Number(price) <= 0) { wx.showToast({ title: '价格必须是大于0的数字', icon: 'none' }); return; } // 调用统一封装的后端接口,提交表单 request('/space/publish', 'POST', { title, address, price, longitude, latitude, startTime, endTime }).then(() => { wx.showToast({ title: '发布成功', icon: 'success' }); wx.navigateBack(); }); } });时段选择在界面上用picker组件实现,mode="time"直接绑定到startTime和endTime。价格字段在提交前不仅要做非空校验,还要用isNaN(Number(price))判断是不是合法数字——字符串"abc"直接传到后端,数据库插入时会报类型转换错误,这类错误在前端拦截掉是最划算的。
预约页的逻辑是:首页或列表页点击某个车位,跳转时通过onLoad参数带上spaceId,页面根据车位信息渲染预约时段选择器。用户选好开始和结束时间后,点击预约按钮调用后端下单接口。注意这里要在前端再校验一次结束时间晚于开始时间,不能只依赖后端,用户体验差一大截。
4.3 订单状态的界面呈现:一份状态映射搞定所有按钮
订单列表和订单详情页最头疼的是状态对应的文案和按钮。后端返回的status是0到4的数字,前端直接写一堆if-else是灾难。正确的做法是做一份状态映射对象,把文案、按钮文案、按钮是否可用统一管理。
// 订单状态映射:0待支付 1已预约 2进行中 3已完成 4已取消 const STATUS_MAP = { 0: { text: '待支付', action: '去支付', color: '#ff9900', actionVisible: true }, 1: { text: '已预约', action: '入场', color: '#07c160', actionVisible: true }, 2: { text: '进行中', action: '出场', color: '#10aeff', actionVisible: true }, 3: { text: '已完成', action: '', color: '#999999', actionVisible: false }, 4: { text: '已取消', action: '', color: '#999999', actionVisible: false } };有了这份映射,WXML里一个{{STATUS_MAP[item.status].text}}就把状态文案渲染出来,按钮显隐用actionVisible控制。按"去支付→入场→出场"的顺序,用户操作一次订单,状态向前推进一格,每步对应后端一个状态流转接口。这里的经验是:不要让前端自己推断状态,比如"支付成功后自动把页面改成已预约"——如果支付成功但后端写库失败,界面的状态就是假的。一切以接口返回为准,页面每次onShow重新拉订单列表,保证状态和服务器一致。
5. 避坑指南:从本地启动到并发下单的经典翻车现场
这一章的每一条都是实际跑这类SSM项目时高概率踩中的坑。按出现频率排序,从环境启动到业务并发,能避过去基本就告别了大部分调试时间。
5.1 Invalid bound statement (not found):Mapper和XML失联
现象:项目启动正常,但一调用某个接口就报Invalid bound statement (not found),具体指向某个Mapper方法。原因:接口方法在Mapper.java里定义了,但对应的XML里没有这个id,或者XML文件根本没被MyBatis扫描到。解决:先检查application.properties里的mybatis.mapper-locations是否指向了classpath:mapper/*.xml,再看接口方法名和XML的id是否一模一样。这个报错的本质是Java接口和XML映射断开了,调试路线就是确认两者互相能找到。
5.2 小程序真机请求失败:url not in domain list
现象:开发者工具里接口调得好好的,真机预览就请求失败,报url not in domain list。原因:微信对wx.request的域名有白名单校验,开发工具可以勾选"不校验合法域名",真机上没有这个选项。解决:开发阶段用本地局域网IP调试,上线前把后端接口部署到HTTPS域名并在小程序后台配置request合法域名。本地联调时让手机和电脑连同一个WiFi,BASE_URL改成电脑的局域网IP,后端Tomcat监听0.0.0.0,就能真机调试完整流程。
5.3 并发下单导致超卖:乐观锁不是可选项
现象:两个用户几乎同时点预约,后端日志显示两个请求都通过了冲突检测,都创建了订单,车位被卖了两次。原因:冲突检测的SELECT和下单的INSERT之间没有加锁,两个请求都读到"无冲突",然后同时插入。解决:回到3.3的lockById,用UPDATE ... WHERE status = 1这种条件更新,影响行数为0就拒绝。这条经验血泪教训——永远不要在并发场景下只依赖"先查再插",必须有一个原子操作兜底。
5.4 订单时间早8小时:时区与JDBC连接串
现象:小程序端传的预约时间是14:00,数据库里存成了6:00。原因:服务器或MySQL的时区设置不对,JDBC连接串缺时区参数。解决:JDBC URL上加serverTimezone=Asia/Shanghai,同时检查MySQL全局时区:
show variables like '%time_zone%';常见做法是jdbc:mysql://localhost:3306/parking_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,这样日期时间就统一成东八区了。这类时区问题在日志里特别有迷惑性,因为System.currentTimeMillis()永远是对的,只有写入数据库后才暴露。
5.5 code2session返回40029:code只能用一次
现象:登录接口偶发报错,微信返回40029 invalid code。原因:前端把同一个code发了两遍,或者两个接口同时调登录,后端第二次用同一个code去换openid就会被微信拒绝。解决:登录接口加防重入,小程序端确保wx.login成功后只调用一次后端登录接口,网络超时不要盲目重试,而是重新走一遍wx.login拿新code。这类问题在真机上比开发工具更容易出现,因为真机网络延迟高,用户可能连点两次登录按钮。
6. 部署验证与进阶方向:跑通全流程后再谈扩展
6.1 本地跑通的最小部署清单
先把环境准备齐:JDK 8以上、Maven、Tomcat 8.5以上、MySQL 5.7以上、微信开发者工具。拿到源码包后,第一步导入SQL脚本建库建表,第二步改后端jdbc.properties里的数据库账号密码,第三步mvn package打出war包扔进Tomcat的webapps目录,第四步小程序端改BASE_URL指向后端地址。启动顺序是先MySQL、再Tomcat、最后微信开发者工具,中间任何一步失败就按第5章的排查路径走。
6.2 六步全流程验证
系统跑起来后,按这条路径完整走一遍才能算真正通了:注册登录 → 发布一个车位 → 在列表页搜到它 → 预约一个未来时段 → 模拟支付 → 入场 → 出场 → 订单变成已完成。每一步去数据库看一眼对应表的记录:登录后t_user多了一行;发布后t_parking_space多一行且status=1;下单后t_order有记录且车位status=0;订单取消后车位status要恢复为1。这条链路验证完,这套系统的主干功能就是可信的。
6.3 三个值得做的进阶方向
第一个是按分钟计费:把固定的每小时价格改成阶梯计价,订单结束后按实际停车时长结算。第二个是微信订阅消息:用户预约成功后给他发一条预约成功通知,入场前再提醒一次,这个功能在微信后台配置模板即可,不需要额外引入推送服务。第三个是真实微信支付对接:统一下单加支付回调验签,注意个人主体的小程序接入微信支付有限制,课程设计阶段保留模拟支付更务实。
我自己第一次做类似项目时,订单状态机没画清楚就急着写代码,结果"取消订单后车位状态没恢复"这种边界问题反复改了两天。后来学乖了,先在一张纸上把状态流转和每个接口的前置条件列清楚再动手,两个下午就写完了主体功能。这套共享停车位系统的复杂度恰好卡在"有业务深度但不至于失控"的位置,把订单状态定义清楚,比写出多少行代码都重要。希望帮到你。
本文还有配套的精品资源,点击获取