尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot+Vue前后端分离的校园一卡通消费系统实战

基于SpringBoot+Vue前后端分离的校园一卡通消费系统实战 简介一套基于SpringBootVue前后端分离架构的一卡通消费系统源码面向Java Web方向毕设与课设学生覆盖人脸识别、刷码、实体卡等常见校园消费场景。压缩包共748个文件包含380个Java后端逻辑、92个Vue页面组件、82个JS交互脚本、55个XML配置及少量YML/Properties环境文件整体仅1.67MB目录结构清晰便于快速定位前后端代码。当前已有88人学习浏览属于中等偏基础难度源码经本地编译验证下载后按文档配置环境即可运行。压缩包还提供run.bat、package.bat等启动与打包脚本适合快速部署体验借助该资源可系统理解SpringBoot接口开发、Vue前端联调、MyBatis数据库映射等典型环节也可参照其分层方式改造扩展作为毕业设计或课程设计的完整参考。 校园里排队打饭的场景大家都经历过有人掏实体卡有人亮付款码还有人直接刷脸。如果把这些方式整合到一套系统里后台还能实时看到每笔消费记录这就是今天要聊的项目——基于SpringBoot Vue前后端分离架构的一卡通消费系统。它同时支持人脸识别、二维码刷码、实体IC卡三种消费方式覆盖了目前校园、企业园区、食堂等场景下的绝大多数支付需求。这套系统从技术选型上走的是目前Java全栈最主流的路线后端SpringBoot负责业务逻辑和接口前端Vue负责页面交互前后端通过RESTful API通信。对于正在做毕业设计、或者刚入职想了解企业级项目怎么搭的同学来说这是一个非常完整的参考案例。它不只是简单的增删改查而是把硬件设备接入、第三方SDK集成、高并发扣款、账户流水记录这些真实业务中都跑一遍。1. 项目整体设计与架构思路1.1 为什么选择前后端分离架构在动手写代码之前第一个要回答的问题是为什么要用前后端分离而不是传统的服务端渲染模板方案一卡通消费系统和普通的后台管理系统不太一样。它的使用场景分为两类一类是消费终端食堂窗口的扫码盒子、人脸识别闸机、刷卡POS机另一类是管理后台充值、对账、用户管理、消费记录查询。终端设备需要的是轻量级、响应快的交互界面而管理后台需要的是复杂的数据展示和操作。如果用Thymeleaf这类服务端渲染方案每台终端设备都要依赖后端渲染页面服务器压力大页面交互也不够流畅。前后端分离之后前端静态资源可以部署到Nginx上后端只提供JSON接口。消费终端通过HTTP请求调用后端接口不管是扫码枪、人脸识别终端还是普通浏览器都可以轻松接入。而且前端Vue的组件化开发方式特别适合这种多终端的场景公共组件如支付弹窗、用户信息展示可以反复复用开发效率提升明显。1.2 三种消费方式的场景定位支持人脸、刷码、实体卡三种方式不是简单的功能堆砌而是基于真实场景需求设计的。实体IC卡是最传统的方案优点是离线可用、识别速度快适合食堂高峰期快速扣款。二维码是近几年的主流用户手机亮码终端扫描后完成支付优点是不用带卡缺点是需要网络支持。人脸识别最方便用户什么都不用带走到摄像头前就能完成支付但对硬件要求高还需要处理光线、角度等图像质量的问题。三种方式互补覆盖了不同用户群体的使用习惯学生喜欢刷码教职工习惯刷卡而人脸识别可以作为忘记带卡和手机时的兜底方案。在设计数据库时这三种方式统一映射到同一个用户账户上也就是说无论你用哪种方式消费扣的都是同一个账户里的钱。2. 后端SpringBoot核心模块设计2.1 系统分层与包结构规划后端采用标准的Controller-Service-Mapper三层架构包结构如下com.campus.card ├── controller # 接口层 ├── service # 业务逻辑层 ├── mapper # 数据访问层 ├── entity # 实体类 ├── dto # 数据传输对象 ├── config # 配置类 ├── common # 通用工具和常量 └── handler # 全局异常处理在实际编码时我建议把用于接收前端请求的参数对象和数据库实体类分开避免前端参数结构变化时影响数据库实体。比如消费接口接收的参数包括消费方式、终端编号、消费金额、识别凭证这些字段需要组合成一个ConsumeRequestDTO而不是直接操作User实体。2.2 三种消费方式的统一鉴权流程无论哪种消费方式核心流程都是三步识别身份、校验账户、扣款记账。区别仅仅在于“识别身份”这一步怎么实现。我把身份识别抽象成了一个接口三种消费方式分别实现public interface IdentityResolver { // 返回用户在系统中的唯一ID Long resolveIdentity(String credential); } Component public class CardIdentityResolver implements IdentityResolver { Override public Long resolveIdentity(String credential) { // 实体IC卡刷卡的卡号直接从卡芯片读取 return cardMapper.getUserIdByCardNo(credential); } } Component public class QrCodeIdentityResolver implements IdentityResolver { Override public Long resolveIdentity(String credential) { // 二维码的内容是一串动态令牌需要解密和校验有效期 String userId qrCodeService.parseAndVerify(credential); return Long.valueOf(userId); } } Component public class FaceIdentityResolver implements IdentityResolver { Override public Long resolveIdentity(String credential) { // credential是前端人脸识别SDK返回的用户特征值或用户ID // 可信终端直接传入userId非可信终端需要调用人脸比对服务 return faceRecognitionService.verify(credential); } }这样设计的好处是当需要新增一种消费方式时比如以后加指纹支付只需要新增一个IdentityResolver的实现类不动原有代码符合开闭原则。2.3 扣款与流水记录的一致性保障消费扣款是整个系统的核心也是最容易出现Bug的地方。很多人一开始容易犯的错误是先查余额余额够就扣款然后插入流水。这段逻辑在并发现场一定会出问题。想象一下这个场景用户在1秒内连续刷了两次码两个请求同时到了后端同时读到余额是5块消费金额都是3块两次都判断余额够都执行了扣款最后余额变成了-1块。解决这个问题有几种方案。第一种是在扣款SQL语句中加入余额条件UPDATE user_account SET balance balance - #{amount} WHERE user_id #{userId} AND balance #{amount}如果影响行数为0说明余额不足或账户异常返回扣款失败。这种方式在单库场景下已经足够不需要引入分布式锁的复杂度。第二种是使用数据库行锁在扣款前先通过SELECT ... FOR UPDATE锁定账户行确保同一时间只有一个请求在操作该账户。两种方式可以结合使用我自己的实践中是在扣款SQL加余额条件的基础上用Redis做一个简单的用户维度锁来增加一层防护。每笔消费都必须生成流水记录流水表结构如下字段名类型说明idbigint主键user_idbigint用户IDorder_novarchar(64)订单号唯一amountdecimal(10,2)消费金额balance_afterdecimal(10,2)消费后余额consume_typetinyint1-实体卡 2-扫码 3-人脸terminal_novarchar(32)终端编号create_timedatetime消费时间订单号的生成建议使用“日期终端编号随机数”的方式避免使用数据库自增ID作为订单号暴露业务量。3. 前端Vue实现要点3.1 项目初始化和环境配置前端使用Vue CLI创建项目Node版本建议使用16以上npm镜像切换到国内源。创建项目的命令很简单vue create card-frontend在交互式配置中选择Vue Router和Vuex或Pinia作为插件。这里有一个实操建议如果你的团队成员对Vue 2比较熟悉就选Vue 2如果是新项目且有时间学习直接上Vue 3 Composition API Pinia。就我目前的经验来看Vue 3的组合式API在代码组织和逻辑复用上确实比Vue 2的选项式API更清晰尤其是消费终端这种需要频繁操作状态的场景。开发环境的API代理配置是前后端分离项目中必须处理的环节。在vue.config.js中配置devServer代理module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } }配置后前端请求/api/user/info会被转发到http://localhost:8080/user/info解决了开发环境的跨域问题。生产环境则用Nginx来做反向代理前端静态资源交给Nginx托管/api路径转发到后端服务。3.2 核心页面和组件拆解整个系统的前端页面分为三个部分用户端消费页、管理后台页面、终端设备适配页。用户端消费页是用户扫码后看到的确认页面主要包括用户信息、余额展示、消费金额输入。这个页面不是重点重点在管理后台。管理后台的页面结构是经典的侧边栏顶栏布局左侧菜单包括用户管理、账户管理、消费记录、终端管理、统计报表、系统设置。每个菜单对应的页面组件放在view目录下公共组件放在components目录下。这里要说一下组件封装的心得。消费记录查询页有一个日期范围选择器用户管理和账户管理页面也有类似的筛选条件。如果每个地方都写一遍筛选逻辑代码会非常冗余。我封装了一个PageWrapper组件统一处理分页、搜索条件、刷新操作各页面只需要传入自己的表格列配置和查询接口即可。封装完以后新增一个管理页面的工作量从半天缩短到两小时。3.3 路由权限与登录状态管理后台管理系统必须有登录拦截和权限控制。在Vue Router的路由配置中给需要认证的页面添加meta: { requiresAuth: true }标记然后在路由守卫中统一校验router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ path: /login }) } else if (to.path /login token) { next({ path: / }) } else { next() } })Axios请求需要统一拦截器在请求头中携带Token在响应中处理Token过期和401状态码。如果后端返回401说明Token已经失效需要清除本地登录状态并跳转到登录页面。登录后的用户信息建议放在Vuex或Pinia中避免每个页面都去LocalStorage读取。需要注意的是LocalStorage只能存不敏感的信息用户的手机号、余额等敏感数据必须通过接口获取不能缓存在前端本地。4. 三种识别方式的技术实现与选型4.1 实体IC卡的接入方案实体IC卡使用的是M1卡或CPU卡读卡器通过串口或USB接口连接终端设备。后端需要实现的接口是接收读卡器上传的卡号。读卡器通常使用韦根协议或串口协议底层通信不需要后端直接处理而是由终端设备如POS机读取卡号后调用后端接口。后端的核心逻辑是维护card表表中保存卡号和用户ID的绑定关系。卡片还可以设置状态正常、挂失、注销。挂失的实现很简单就是在卡表中把状态改为挂失消费时校验状态即可。这里要注意一个安全细节读卡器上传的卡号必须做校验不能直接信任终端设备传来的数据。典型的做法是终端设备需要向后端申请一个临时Token然后使用这个Token调用消费接口。也就是说终端设备在请求头中携带自身设备的Token后端再去判断这个终端编号是否有权限发起扣款。4.2 二维码刷码的实现逻辑二维码消费有两种模式用户被扫用户出示二维码终端扫描和用户主扫用户扫终端上的二维码。一卡通消费系统中常用的是被扫模式用户打开手机APP或小程序展示一个二维码终端扫码后上传二维码内容到后端。二维码的内容不能是简单的用户ID明文否则二维码被别人拍到后任何人都能拿着去消费。正确做法是生成一个动态令牌有效期为60秒或者使用一次即失效用户点击“生成付款码”时前端调用后端接口获取token。后端生成一个随机字符串token将token和用户ID的映射关系存储到Redis中并设置过期时间为60秒。终端扫码后将token上传到后端。后端从Redis中查询token对应的用户ID如果不存在判定为无效码。用Redis存储token的好处是天然支持过期时间符合二维码“短时有效”的语义。在并发扣款时还可以利用Redis的原子性操作确保同一个token只能被使用一次避免重复扣款。4.3 人脸识别接入的两种路径人脸识别是整个系统中最具技术含量的部分也是踩坑最多的地方。市面上人脸识别的方案主要分为两种第一种是离线的SDK方案。终端设备集成人脸识别SDK如百度离线 SDK、虹软ArcFace在设备本地完成人脸检测和特征提取人脸特征值存到后台。消费时终端摄像头采集人脸SDK提取特征值并和本地缓存的特征值比对比对成功后将用户ID上传到后端完成扣款。这种方案的优点是快、不依赖外部网络缺点是SDK需要购买授权终端成本高。第二种是在线API方案。终端设备把人脸图片上传到后端后端调用云服务商的人脸识别API完成比对。优点是终端硬件要求低缺点是每笔消费都会产生API调用费用而且网络延迟会直接影响消费体验高峰期可能会出现排队等情况。综合考虑成本、识别速度和离线容错能力我建议采用离线SDK方案。实际项目中选择哪家SDK要看设备的算力大小和采购预算。识别阈值参数也非常讲究阈值设得太高识别率低用户经常要重复刷脸设得太低则有可能认错人造成资金风险。一般建议初始值设为0.7左右然后根据实际场景微调。光线较暗的窗口需要降低阈值户外场景则要提高阈值防止误识别。5. 项目实操过程与核心环节实现5.1 后端环境搭建过程后端开发环境是JDK 1.8或11 MySQL 5.7或8.0 Redis。使用IDEA创建SpringBoot项目时有时会遇到下载依赖超时的问题这通常是因为Maven中央仓库连接不稳定。解决方法是在Maven的settings.xml中配置阿里云镜像mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/central/url /mirror配置文件application.yml中包含数据源、Redis、MyBatis映射路径等核心配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/card_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.campus.card.entity数据库的初始化脚本包含用户表、账户表、卡片表、消费流水表、终端表、管理员表等核心表结构。在实际开发中我习惯用数据库迁移工具如Flyway来管理表结构的变更团队成员拿到项目后执行一条命令就能建库建表效率比传SQL文件高很多。5.2 消费接口的完整业务流程一个完整的消费请求链路是这样的终端设备请求后端消费接口接口接收请求参数消费方式、终端编号、识别凭证、金额进行身份识别、状态校验、锁账户、扣款、记流水、响应结果。以下是简化后的消费接口核心代码展示了关键的流程控制逻辑PostMapping(/consume) public ResultVo consume(RequestBody ConsumeRequestDTO request) { // 1. 校验终端是否有权限发起消费 Terminal terminal terminalMapper.selectByTerminalNo(request.getTerminalNo()); if (terminal null || terminal.getStatus() ! 1) { return ResultVo.error(终端不合法或已停用); } // 2. 根据消费方式解析用户身份 IdentityResolver resolver resolverFactory.getResolver(request.getConsumeType()); Long userId resolver.resolveIdentity(request.getCredential()); if (userId null) { return ResultVo.error(身份识别失败); } // 3. 加锁防止并发扣款 String lockKey consume:lock: userId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 3, TimeUnit.SECONDS); if (!locked) { return ResultVo.error(操作频繁请稍后重试); } try { // 4. 扣款SQL中带余额条件确保金额正确 int rows accountMapper.deductBalance(userId, request.getAmount()); if (rows 0) { return ResultVo.error(余额不足); } // 5. 生成消费流水 BigDecimal balanceAfter accountMapper.selectBalance(userId); consumeRecordMapper.insert(userId, generateOrderNo(), request.getAmount(), balanceAfter, request.getConsumeType(), request.getTerminalNo()); return ResultVo.success(orderNo); } finally { redisTemplate.delete(lockKey); } }这里的几个细节值得展开来说。先校验终端再识别身份避免无效请求打到识别模块Redis锁的超时时间设置为3秒识别和扣款的整体耗时不会超过这个值扣款SQL带余额条件是防止超扣的最后一道防线流水插入放在扣款成功之后确保账实相符。5.3 前端页面的实现与接口联调前端开发时我习惯按照“登录页 → 布局框架 → 首页仪表盘 → 用户管理 → 消费记录 → 终端管理”的顺序来推进。登录页用Element UI的表单组件校验规则用async-validator提交后调用后端登录接口获取Token并将用户信息存入Pinia。管理后台的首页仪表盘展示今日消费总额、消费笔数、活跃终端数、账户总数等统计数据。这些数据的计算如果每次实时查数据库会对数据库造成比较大的压力。我的方案是使用Redis的HyperLogLog统计每日活跃用户数消费总额则通过定时任务每小时汇总一次存入统计表页面展示时直接查统计表即可。接口联调阶段最常遇到的问题就是跨域和字段命名不一致。后端返回的字段用的是驼峰命名如balanceAfter如果后端接口的JSON序列化配置没有开启驼峰映射前端就会拿不到数据。在SpringBoot的application.yml中加入以下配置解决spring: jackson: property-naming-strategy: SNAKE_CASE这样后端返回的字段名会自动转为下划线风格如balance_after前端代码中统一使用下划线风格访问即可。必须保证前后端开发时先约定好接口文档我用的是YApi或者Apifox接口定义确认后再开发能减少后期联调的大量返工。6. 常见问题与排查技巧实录6.1 前端接口请求跨域报错前后端分离开发中跨域是最常见的问题。浏览器控制台报Access-Control-Allow-Origin错误时不要一上来就在后端加CrossOrigin注解先分清是开发环境还是生产环境的问题。开发环境的跨域用代理解决vue.config.js中配置devServer的proxy即可。但这里有一个容易踩的坑如果你的页面是通过localhost:8081访问前端API请求转发到的是http://localhost:8080那后端的日志中看到的访问来源还是前端服务因为代理转发是服务端行为不涉及浏览器跨域。生产环境的跨域通过Nginx配置解决server { listen 80; server_name card.example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }其中try_files配置是为了解决Vue Router的history模式刷新后404的问题proxy_pass末尾的斜杠表示将/api前缀替换为空。6.2 扫码消费提示“无效二维码”这个问题的排查路径比较固定。先看前端是否正常请求了生成二维码接口再看后端Redis中是否存在对应的token最后看消费时上传的token和生成的是否一致。我遇到过一种比较隐蔽的情况用户同时打开了两个页面每个页面都生成了一个二维码但用户第一次展示的是旧页面的二维码这个token在Redis中已经过期或不存在。排查时发现二维码内容里包含了时间戳前端在展示二维码时还可以加一个倒计时条提醒用户二维码即将失效。还有一种情况是终端扫码到上传之间的网络延迟太长超过了token的有效期。解决方法是把token有效期从60秒调整为120秒同时增加一个“窗帘拉取”机制用户在进入支付页时预生成一个二维码当二维码即将过期时自动刷新。6.3 人脸识别在光线不足时识别率下降这是人脸识别场景中最常见的用户体验问题。光线不足、背光环境下摄像头采集到的人脸图像质量差SDK提取的特征值不可靠导致识别失败率明显上升。我的解决思路是硬件和软件结合。硬件方面在人脸识别终端上加装补光灯尤其是在就餐高峰期和大堂入口处。软件方面将SDK的图像质量检测功能开启如果检测到图像过暗返回一个提示信息给用户而不是直接进入比对流程。另外人脸特征的注册质量也非常关键。很多项目在注册人脸时只是让用户随意拍一张这样后期识别率很难保证。合理做法是指引用户正对摄像头、保持自然表情、移除眼镜和帽子连续采集多帧图像从中选一张质量最高的作为注册底图。实测下来这样处理后的注册照片能显著提高后续识别的通过率。6.4 高峰期消费接口响应变慢一卡通消费系统的高峰期非常集中食堂的午餐时段可能全校几千人同时集中在40分钟内消费。这时候最容易成为瓶颈的就是数据库的账户表和流水表。除了前面提到的Redis锁数据库层面还有几个优化手段。第一个是给流水表按日期分表每天一张表例如consume_record_20250101查询历史流水时按日期定位到对应分表避免单表数据量过大。第二个是在流水表的查询字段user_id、create_time上建立联合索引让“查某用户某天的消费记录”这类高频查询走索引。第三个是连接池参数的调整MySQL默认的max_connections是100高峰期很容易打满可以调整为500但也要注意不要盲目调太高否则数据库本身可能会被压垮。7. 部署要点与压测心得项目开发完成后部署上线是最后一个环节。整套系统的部署架构是前端静态文件部署到Nginx后端SpringBoot打成Jar包部署在服务器数据库用MySQL缓存用Redis。生产环境的配置要和开发环境隔离。SpringBoot项目中创建application-prod.yml配置文件用spring.profiles.activeprod指定生产配置。密码等敏感信息不要明文写在配置文件中可以使用Jasypt加密配置文件中保存密文启动时通过特定的密钥解密。上线前要做一波简单的压力测试。用JMeter模拟高并发消费请求重点观察接口的TPS、响应时间P99、数据库连接池使用情况。我这边实测下来的数据4核8G的单机部署加上Redis缓存和数据库连接池优化消费接口的TPS大约能达到600左右如果以后用户量增长可以通过增加后端实例配合Nginx负载均衡来水平扩容。最后再说一个实操中的小细节系统的定时任务比如每日账单结算、过期二维码清理用SpringBoot自带的Scheduled注解就可以实现。别一上来就引入xxl-job这类分布式任务调度框架单机场景下标准JDK就能很好地完成架构过度设计反而增加了维护成本。等真正到了需要多节点部署且定时任务又存在重复执行问题时再迁移到分布式调度不迟。本文还有配套的精品资源点击获取
返回列表