SpringBoot+微信小程序校园一卡通系统:并发扣款与支付对接实战
看到springboot基于微信小程序的高校校园一卡通系统_hs7djwul这种带随机后缀的命名懂的人都知道这是个典型的毕设选题。我当初做这个项目时以为核心工作量在小程序界面和 CRUD 接口上结果真正动手之后才发现校园一卡通系统最考验人的地方根本不在“能不能跑通”而是在“账能不能对平”。余额扣重、重复充值、并发扣款、支付回调丢失任何一个小问题都可能导致用户资金异常这在校园场景里是大事故。这篇就把我做这个项目的完整思路、代码方案和踩过的坑梳理出来。无论你是拿它做毕业设计还是想在企业里落地一个类似的校园消费系统都可以顺着这条线复现一遍。我尽量把为什么这么做、为什么不那么做也讲清楚。1. 一卡通系统的业务本质先想清楚这系统到底在管什么1.1 从一张实体卡到一个线上账户体系提到校园一卡通很多人的第一反应是“饭卡电子化”。这个理解没有错但不够。高校一卡通在真实环境里覆盖的消费场景远比“食堂吃饭”要多包括超市购物、浴室淋浴、图书馆门禁、机房上机、水电费缴纳、校车乘坐等等。线上化之后这张卡的本质就变成了一个预付费账户体系用户先往账户里充值再在不同场景里消费系统负责保证每一笔余额变动都精确无误。在我做的这个系统里核心参与角色有三种学生/教职工用户端在小程序里登录绑定校园卡查余额、查流水、充值和消费。商户/消费终端应用端食堂、超市、浴室等场景的收款设备或后台操作台负责发起扣款请求。系统管理员管理端负责用户管理、卡片状态管理、订单管理、数据统计和对账。这个定位直接决定了后端模块怎么划分。不要在开发前急着写代码先把这几个角色的核心诉求列出来再倒推表结构和接口设计后面可以少走很多弯路。1.2 和普通电商系统的本质区别在哪里有电商开发经验的人可能会说不就是一个充值 扣款 账单的功能组合吗如果真这么想后面大概率会在细节上翻车。高校一卡通系统有三个特别之处。第一消费场景离线容忍度高但对账要求极高。食堂刷卡这种场景终端设备可能存在断网情况扣款请求可能延迟到达甚至终端本地先记账后台再同步。这就要求每一笔交易都必须有唯一流水号平台侧要能兜住延迟数据、重复数据和乱序数据。第二并发集中在极短时间窗口。下课高峰期一个食堂可能同时几千人刷卡同一个账户可能在一分钟内收到多笔并发扣款请求。余额扣减必须做到并发安全否则就会出现“余额只有 10 块却同时刷了两笔 8 块的订单”这种超扣问题。这一点在第三章节我会单独展开讲方案。第三用户的资金敏感度极高。学生群体对账目问题非常敏感一卡通里哪怕少了一块钱投诉就可能立刻到辅导员那里。系统必须提供清晰透明的流水查询每一笔充值、每一笔消费、每一笔退款都能追溯到时间、地点和操作人。1.3 第一版功能范围的划分做项目最忌讳的就是一上来就想做全。我当时的做法是把系统切成最小可用版本MVP先把核心闭环跑通再逐步加外围功能。第一版我建议你重点实现以下模块小程序端的微信登录 校园卡绑定/解绑账户余额查询、交易流水查询余额充值对接微信支付扫码/输入卡号消费扣款商户端管理后台的用户管理、卡片冻结/挂失、基础数据统计至于消息推送、消费趋势分析、多校区数据汇总、水电缴费这类功能完全可以等核心链路稳定之后作为二期、三期再迭代。这个习惯能让你在有限的时间里把主干质量做扎实而不是做出一堆半成品功能。2. 技术选型复盘SpringBoot 微信小程序这个组合好在哪里2.1 后端为什么选择 SpringBoot技术选型这件事在毕设和实际项目里的考量因素不太一样。但 SpringBoot 对于这类系统来说确实是综合成本最低的选择。首先是生态成熟。SpringBoot 集成了几乎你能想到的所有中间件和数据库访问层方案无论是 MyBatis-Plus、Spring Data JPA还是 Redis、RabbitMQ、MinIO都有非常完整的官方文档和社区资料。遇到问题搜索引擎一查就是解决方案这对项目开发效率的影响是决定性的。其次是敏捷开发体验好。SpringBoot 的自动配置机制和起步依赖设计让开发者可以快速搭起一个可运行的项目骨架。我记得当时用 IDEA 初始化项目再引入 Web、MyBatis-Plus、MySQL、Redis、Validation 这些依赖从零到跑起一个带健康检查的接口十几分钟就搞定了。这种体验对需要控制开发周期的项目来说非常友好。第三是部署运维门槛低。打包成 JAR 直接运行内置 Tomcat不需要额外安装容器。对于最终要部署到一台服务器上的项目来说这种“一个 Java -jar 就上线”的方式非常省事。我在后面的部署章节会详细讲怎么配置生产环境。2.2 小程序端为什么比 App 更契合校园场景前端我也认真权衡过做原生 App 还是做微信小程序结论很明确选小程序。首先是免安装。高校用户群体极度偏好轻量应用让一个学生为了查余额专门去应用商店下载一个 App想想都知道转化率会有多低。小程序扫码即用、用完即走在校园食堂这种线下场景里体验感好很多。其次是微信生态能力。小程序可以直接复用微信登录能力做身份识别可以直接使用微信支付完成充值闭环还可以通过订阅消息向用户推送余额变动提醒。这些能力如果自己做 App每一样都需要花费额外的开发和备案成本。第三是跨端成本低。如果你之前接触过 uni-app 这类跨端框架会发现从微信小程序扩展到支付宝小程序、H5 的成本非常低一套代码多处运行。这对未来系统扩展是一个很好的铺垫。2.3 我最终采用的工程模块划分我最终项目结构大致是这样的你拿到手直接抄作业也行hs7djwul-admin管理后台 API面向管理员和商户端操作台SpringBoot 模块hs7djwul-app小程序端前端代码原生微信小程序或 uni-app 均可hs7djwul-common公共模块包含统一返回结构、异常处理、工具类、常量定义hs7djwul-core核心业务模块包含用户、卡片、订单、交易流水、商户等业务逻辑hs7djwul-payment支付模块封装微信支付 v3 的统一下单、回调、退款逻辑前后端用 JSON 交互统一返回格式定义为{ code, message, data }所有异常由全局异常处理器统一捕获转换。这个约定让我后期前后端联调时省了特别多扯皮的时间。3. 数据库设计校园卡资金安全从建表开始3.1 核心表结构设计一卡通系统的数据库设计最重要的原则是把流水和余额分开存。余额是当前状态流水是变化历史。任何时候余额对不上都可以用流水重算校验。这是银行等金融系统里最基本的思路校园卡本质上也应该这么设计。我项目中的核心表大致包括t_user用户表小程序 openid、姓名、学号/工号、手机号、身份类型学生/教职工/商户、状态等t_card卡片表卡号、用户 ID、余额、卡状态正常/挂失/冻结/注销、版本号并发控制用、绑定时间t_account_flow交易流水表流水号、卡号、用户 ID、业务类型充值/消费/退款/系统调整、变动金额、变动后余额、关联订单号、交易时间、备注t_recharge_order充值订单表订单号、用户 ID、卡号、充值金额、支付状态、微信支付单号、回调时间t_merchant商户表商户编号、商户名称、所属校区/食堂、状态t_consume_record消费记录表消费单号、卡号、商户编号、消费金额、消费方式扫码/刷卡/手动输入、消费时间、流水号我画一下简化版的建表语句重点看字段取舍CREATE TABLE t_card ( id bigint NOT NULL AUTO_INCREMENT, card_no varchar(32) NOT NULL COMMENT 校园卡号, user_id bigint NOT NULL COMMENT 用户ID, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 余额, status tinyint NOT NULL DEFAULT 1 COMMENT 1正常 2挂失 3冻结 4注销, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_card_no (card_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT校园卡表; CREATE TABLE t_account_flow ( id bigint NOT NULL AUTO_INCREMENT, flow_no varchar(64) NOT NULL COMMENT 流水号, card_no varchar(32) NOT NULL, user_id bigint NOT NULL, biz_type tinyint NOT NULL COMMENT 1充值 2消费 3退款 4系统调整, change_amount decimal(10,2) NOT NULL COMMENT 变动金额正数加钱负数扣钱, balance_after decimal(10,2) NOT NULL COMMENT 变动后余额, order_no varchar(64) DEFAULT NULL COMMENT 关联单号, remark varchar(255) DEFAULT NULL, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_flow_no (flow_no), KEY idx_card_no (card_no), KEY idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账户流水表;注意几个细节金额字段一律用decimal(10,2)不要用float或double否则会出现金额精度丢失这在资金系统里是致命问题。流水号用UNIQUE约束兜底业务层面可以尽量做到不重复但数据库层面也要有最后一道防线。所有核心表都有create_time和update_time并且遵循card_no做索引。在分页查流水时建议额外建立(card_no, create_time)的联合索引。3.2 为什么必须有“余额快照”这个冗余字段很多人做流水表只记录变动金额觉得查询余额时把所有流水累加就行。这个想法看似优雅实际在长期运行的项目里是个灾难。当流水量到几十万条、上百万条时每次查余额都要全表聚合性能会迅速劣化。更重要的是如果中途某条历史流水被手工修正过累加结果会整体偏移而你不知道偏移发生在哪一笔。所以我的方案是流水表里直接记录每一笔交易发生后的balance_after。这样查询异常时可以通过检查相邻两笔流水的balance_after差值是否等于change_amount快速定位是哪一笔出了问题。这就是“余额快照”的价值它是排查账务异常时最重要的抓手。3.3 初始化数据与测试数据准备建完表之后别急着写代码先做一套完整的初始化数据。这个步骤很多人会跳过但后面联调时你会发现没有它真的太痛苦了。造 3 个学生账号余额分别设置为 0、100、1000造 1 个教师账号造 2 个商户食堂A、超市B每个学生生成一张正常状态的校园卡后期测试并发扣款、余额不足、充值回调、退款等场景时这套基础数据能覆盖绝大多数情况。4. 小程序端登录与身份认证wx.login 只是第一步4.1 三层登录链路微信小程序端的登录不能简单理解成“调一下 wx.login 拿到 code 就完事”。在校园一卡通系统里完整的链路是三层第一层微信身份层。前端调用wx.login获取临时 code后端拿 code 到微信接口服务换取openid和session_key。openid 是这个用户在当前小程序下的唯一身份标识。第二层平台账号层。用 openid 去用户表里查如果查不到就对应用户不存在前端走绑定注册流程如果查到了就说明这个人已经绑定了校园卡可以直接进入主界面。第三层校园卡绑定层。用户输入学号/工号和查询密码或者身份证后六位完成认证后系统把微信 openid 和学校的身份数据绑定起来。从此这个微信号就代表了这个学生的校园卡账户。这三层逻辑如果一开始设计得不够清晰后期很容易出现“一个微信绑了多张卡”“一张卡被多个微信绑定”的混乱情况。我在用户表里给openid和card_no都加了唯一索引这样就从数据库层面杜绝了这种数据问题。4.2 会话保持与 token 设计微信官方方案里wx.login拿 code 换 session_key 是一种短期凭证机制不可能每次请求接口都去微信侧校验身份。因此需要自己签发一个业务 token在小程序端保存下来做后续请求的凭证。我用的方案是后端换取 openid 成功后生成一个 JWT token过期时间设置为 7 天把 openid 和 userId 放进 token 的 claims 里。小程序端每次请求在请求头里带Authorization: Bearer token后端通过拦截器统一解析和校验。这里有一个很容易踩的坑JWT 是无状态的签发之后在过期前无法主动作废。如果需要实现“用户在小程序里退出登录后 token 立即失效”这类需求就需要引入 Redis把 token 的 jtiJWT ID或者 userId 作为 key 存一份拦截器里先查 Redis 再校验 JWT。我在实际项目里采用了“Redis JWT”双校验的方式既能享受无状态扩展性又能灵活失效。4.3 手机号授权与真实身份核验如果系统需要采集用户手机号小程序提供了getPhoneNumber的能力。需要注意这个接口从 2022 年之后就不再无条件开放了需要小程序主体是企业/个体工商户且完成微信认证并开通相应接口权限。个人主体的小程序无法使用这个能力这是我当时被卡了很久的一个限制。在需求方允许的情况下我建议让用户跳过手机号授权只做学号密码绑定。因为手机号并非校园一卡通系统不可获取的核心字段学号和校园卡密码已经是足够强的身份凭证。5. 交易扣款这个核心接口并发、幂等、余额校验5.1 扣款接口面临的真实并发场景校园卡扣款接口是整个系统里最核心、也最容易出事故的接口。假设一个场景午饭高峰期某学生的卡余额是 10.00 元同一毫秒内有两个扣款请求并行到达金额分别是 8 元和 5 元。如果代码不加任何并发控制那么两个请求同时查到余额为 10都判断余额充足然后各自扣款最终余额变成 -3 元。这在真实系统里绝对不允许出现。这类并发问题的解决方案有三种常见思路数据库悲观锁SELECT ... FOR UPDATE、乐观锁版本号或原子条件更新、Redis 分布式锁。三种我都用过简单聊聊适用边界。悲观锁在有索引命中的行记录上性能尚可但事务时间过长时会造成锁等待堆积高峰期可能会把数据库拖垮。Redis 分布式锁上手快但是要注意锁的粒度、过期时间、重入性等一堆细节一旦实现不好更容易出隐性 bug。我的建议是单体应用里优先用乐观锁架构演进到微服务之后再上分布式锁不要一开始就把复杂度拉满。5.2 乐观锁扣款方案的实现最终我的扣款实现采用了 MyBatis-Plus 的乐观锁插件配合自定义 SQL 原子更新。核心逻辑如下Transactional(rollbackFor Exception.class) public ConsumeResult consume(ConsumeRequest request) { // 1. 查询卡片信息带版本号 Card card cardMapper.selectByCardNo(request.getCardNo()); if (card null || card.getStatus() ! CardStatus.NORMAL) { throw new BizException(卡片不存在或状态异常); } // 2. 计算扣款 BigDecimal newBalance card.getBalance().subtract(request.getAmount()); if (newBalance.compareTo(BigDecimal.ZERO) 0) { throw new BizException(余额不足); } // 3. 原子扣款余额必须 扣款金额且版本号匹配才更新 int rows cardMapper.deductBalance(card.getCardNo(), request.getAmount(), card.getVersion(), card.getBalance()); if (rows 0) { throw new BizException(扣款失败请重试); } // 4. 插入流水 String flowNo generateFlowNo(); accountFlowMapper.insert(AccountFlow.builder() .flowNo(flowNo) .cardNo(card.getCardNo()) .userId(card.getUserId()) .bizType(BizType.CONSUME.getValue()) .changeAmount(request.getAmount().negate()) .balanceAfter(newBalance) .orderNo(request.getOrderNo()) .build()); return ConsumeResult.builder().flowNo(flowNo).balanceAfter(newBalance).build(); }对应的 mapper SQL 是一条原子 UPDATEUPDATE t_card SET balance balance - #{amount}, version version 1, update_time now() WHERE card_no #{cardNo} AND version #{version} AND balance #{amount}这条 SQL 是关键它通过WHERE条件同时完成了三重校验卡号匹配、版本号匹配、余额足够才扣款。如果两个并发请求同时进来只有第一个请求能更新成功版本号匹配第二个请求更新行数为 0直接抛出“扣款失败请重试”。这样就从数据库层面彻底杜绝了超扣问题。这里有个小经验虽然 MyBatis-Plus 有Version注解自动处理乐观锁但它默认的做法是先 update 再检查影响行数如果失败不会提示具体原因。我实测下来更推荐在 Mapper 里手写上面这条 SQL因为你能把“余额是否足够”和“版本号是否匹配”这两种失败原因通过影响行数统一拦截再结合业务重新查询判断具体失败原因。5.3 幂等性设计防止同一笔订单被重复消费并发安全只是扣款问题的一半另一半是幂等。所谓幂等就是同一个扣款请求无论被提交多少次最终只产生一次扣款效果。这个需求看起来简单实际非常容易遗漏。举两个真实发生过的场景商户端网络超时收银员点了两次“确认收款”两次请求前后到达后端。如果没有幂等处理消费者的钱会被扣两次。支付回调通知机制本身是不可靠的微信官方会多次推送同一个支付结果通知如果回调处理逻辑没有幂等充值接口会被执行两次。我的解决方案是引入业务订单号。商户在发起扣款前先生成一个全局唯一的订单号orderNo也可以由调用方传入后端在处理扣款前先查一下消费记录表里是否已经存在相同orderNo的记录如果已存在且状态是成功直接返回第一次处理结果不再重复扣款如果已存在但状态是处理中说明上一次处理没有完成可以等待或返回重试提示如果不存在则继续正常扣款流程。同时我在消费记录表的order_no字段上加了一个唯一索引。即使并发来了两个相同订单号的请求数据库底层也会阻止第二条重复记录插入。这两层保障叠加能覆盖绝大多数重复请求场景。5.4 退款与撤销生成退款单别直接改余额退款的实现要比充值和消费更谨慎。真实业务里退款往往还涉及商户对账、财务审计等问题不能简单做一个“余额加回去”的接口就完事。我建议的设计方案是创建一张退款单t_refund_order记录原消费单号、退款金额、退款原因、操作人。执行退款时先校验原消费单是否存在、是否已退款过、退款金额是否超过原消费金额。在同一个事务里扣减余额流水记录change_amount为负数再插入一条退款流水change_amount为正数并在两张单之间建立关联。如果涉及微信支付的退款还需要调用微信支付 v3 的退款接口并把微信退款单号回写到本地退款单中保证两边状态最终一致。退款使用独立退款单而不是直接改余额最大的好处是可对账。哪一天财务要求出退款明细表直接从退款单表拉数据就行系统里每一笔资金的变动都清清楚楚。6. 微信支付 v3 对接与“支付功能暂时无法使用”的排查思路6.1 小程序支付的完整链路充值是一卡通系统的资金入口微信支付 v3 是目前官方推荐接入的版本。完整的支付链路是小程序端点击“充值”请求后端创建充值订单。后端生成业务订单号调用微信支付 v3 的下单接口传入openid、amount、notify_url等参数拿到prepay_id。后端用prepay_id生成 Jsapi 调起支付所需的签名参数返回给小程序端。小程序端调用wx.requestPayment拉起微信支付面板。用户输入密码完成支付微信服务器异步通知后端notify_url。后端收到回调验签、解密、核对订单号更新本地订单状态和用户余额。这个链路里面最容易出问题的点有三个签名、回调验签和金额核对。6.2 支付权限受限时怎么定位问题我遇到过一种很典型的情况后端下单单成功小程序端能拉起支付面板但用户在支付时提示“支付功能暂时无法使用”。这个文案不是 MyBatis 报错也不是 Java 异常而是微信支付侧返回的限制提示。出现这种提示排查优先级应该是这个顺序先看商户号状态。登录微信支付商户平台看商户号是否正常有没有风控限制、欠费或资质审核未通过。看小程序类目和支付权限。如果小程序主体是个人或者类目没有覆盖到“生活服务 校园服务”支付权限是申请不下来的。个人主体小程序目前无法开通微信支付这是硬性规定。看下单接口返回的错误码。下单成功只代表你拿到了prepay_id不代表一定可以支付。很多时候问题出在商户号与小程序的绑定关系上比如商户号没有在小程序后台完成关联。看回调通知。如果支付环节真的扣款成功了但本地余额没变优先检查回调是否收得到、验签是否通过。我自己测试阶段的做法是先用微信开发者工具模拟支付再用真机扫码支付同时在后端专门记录回调日志。在回调处理接口里打印请求头、请求体、签名校验结果这样每次支付回调来了都能看到微信发过来的原始通知结构定位问题会快很多。6.3 测试环境没有真实支付权限时的替代方案如果你是在毕设阶段暂时没有企业主体和商户号怎么办我的建议是做支付接口的适配层不要直接把微信支付代码写死在业务逻辑里。具体办法是定义一个PaymentService接口包含createOrder()和handleNotify()两个方法微信支付实现一个WechatPayServiceImpl测试环境再写一个MockPayServiceImpl。Mock 实现可以模拟下单成功、模拟回调通知返回固定的prepay_id这样前端可以完整走通充值链路后端也能验证充值后的余额变动逻辑。等真正申请到商户号后只需要切换一个 Bean 的生效条件系统就能无缝切换到真实支付。这也是一个很好的“面向接口编程”的实践案例。7. 前后端联调与真机调试光靠模拟器是不够的7.1 开发者工具和真机的差异很多项目在微信开发者工具里一切正常一到真机上就出问题。我遇到过的典型场景包括开发者工具里网络请求通、真机上却请求失败模拟器上定位正常、真机上拿不到位置。其中最坑的一个是——开发者工具默认不校验合法域名但真机模式下必须在小程序后台配置 request 合法域名而且必须是 HTTPS不能带端口。我当时在本地开发时的做法是后端启动时配置一个本地 HTTPS 代理用 Nginx 或者 Caddy 做反向代理加上自签名证书小程序开发者工具里勾选“不校验合法域名”日常联调完全够用。一旦要上真机预览就必须在小程序后台把线上 HTTPS 域名配好否则请求直接失败且报错并不明显。7.2 用日志和代理工具定位接口问题排查接口问题时我最常用的是“两层夹击”后端日志记录完整请求参数和响应时间前端用调试工具看实际发出的请求体和拿到的响应体。两边一对照问题出在前端还是后端立刻见分晓。如果是定位微信小程序自身的网络请求问题开发者工具自带的 Network 面板够用。需要更细致地看请求报文时也可以用代理工具对小程序流量进行抓包分析。要注意的是微信小程序对 HTTPS 证书的校验比较严格抓包工具需要先安装并信任对应的根证书否则小程序会直接拒绝连接。我的习惯是每开发完一个接口先在 Swagger 或者 Postman 里做一轮纯后端测试确认接口本身没有问题再去小程序端接页面。宁可后端多花一点时间也不要让前端在不明所以的接口问题上浪费时间。7.3 联调阶段常见问题清单联调阶段我把踩过的坑整理成了一个小清单贴在这里给读者参考小程序端请求时 header 里没有带Content-Type: application/json导致后端RequestBody解析为 null后端返回的日期格式是yyyy-MM-dd HH:mm:ss小程序端没有做格式化处理直接展示成了 ISO 字符串金额在后端是decimal转成 JSON 后变成了数字前端做浮点运算时出现精度问题小程序端在页面onLoad时发请求但用户还没有登录成功token 为空导致请求 401同一个接口同时被多个页面调用后端没有加缓存导致数据库压力过大、接口响应变慢这些问题都不复杂但每一个都可能卡住半天时间。提前知道就能避免很多无效的“联调加班”。8. 数据安全与权限控制资金系统不能只靠“登录拦截”8.1 用户身份与角色的权限模型校园一卡通系统里的用户类型天然复杂学生、教职工、商户、管理员不同类型能做的事完全不同。我当时用的是 RBAC基于角色的访问控制模型定义了三张基础表t_user、t_role、t_user_role再用一个简单的权限码表控制接口访问。在后端实现上我自定义了一个RequireRole注解标注在 Controller 方法或类上拦截器解析 JWT 里的 userId 后查询用户角色再判断是否允许访问。这种轻量级方案对单体项目足够不需要引入 Spring Security 和 Shiro 这类重型框架。8.2 防重放、防篡改、防越权资金系统的接口光有登录拦截还不够至少还要考虑三个层面的攻击防护防重放同一个扣款请求被抓包后重复提交如果系统不做幂等就可能造成重复扣款。前面说的order_no唯一索引就是防重放的第一道防线。防篡改小程序端传上来的金额、卡号这些数据不能完全信任。比如扣款接口商户端可以传amount但后端必须校验金额范围比如单笔不超过一定额度充值接口后端应该以下单时传入的金额为准而不是信任回调通知里的原始参数。防越权用户查询流水时只能查自己卡片的流水。如果接口写成了GET /flow?cardNoxxx而用户又能随意传cardNo那么任意用户都可以遍历别人的账户流水。正确做法是从 token 里解析出当前用户的 openid再通过 openid 反查自己名下的卡号而不是信任前端传参。8.3 敏感数据存储规范用户手机号、身份证号这类敏感信息不能明文存储。我当时对手机号做了 AES 加密存储对外展示时只显示前三位和后四位身份证号在绑定校园卡的时候只用于后台校验校验通过后不落库直接丢弃。这种处理不仅是技术问题也是合规要求。如果你在真实高校环境里部署系统数据合规的检查是躲不掉的早设计早省事。9. 部署、日志与后续演进方向9.1 生产环境的部署结构我的部署方案比较常规但足够稳定一台云服务器2核4G起步操作系统 Ubuntu 22.04后端 SpringBoot 应用打包成 JAR用 systemd 托管设置开机自启和崩溃自动重启数据库使用云厂商提供的 MySQL 实例开启自动备份Redis 用于 token 存储和接口缓存Nginx 做反向代理配置 HTTPS 证书把 API 请求转发到后端端口小程序前端代码通过微信开发者工具直接上传不需要自己维护静态服务器一个让我印象很深的经验是上线之前一定要把生产环境的spring.profiles.activeprod配置好把数据库连接、Redis 地址、日志级别都拆成独立配置。很多人本地跑得好好的一到服务器就各种连不上基本都是环境配置问题。9.2 日志规范与问题追查资金系统必须记录高质量日志。我项目里的统一做法是所有核心接口在入口和出口各打印一条日志包含请求方标识、操作类型、请求参数脱敏、处理耗时、处理结果。一旦线上出了问题日志就是唯一的破案线索。我给自己定的标准是不看代码、不看数据库单靠日志就能还原一笔从“请求进来”到“余额扣减”的完整过程。这个标准听起来高但其实只需要在写代码时多花一点心思把关键节点都打印出来即可。9.3 从毕设到可用系统还能往哪里演进除了核心功能这个项目天然适合继续扩展的方向还有不少消息通知通过微信订阅消息在充值成功、消费超过阈值、卡片挂失时主动通知用户一码通把校园卡从“支付”扩展到“门禁”“图书馆借阅”“会议签到”等多个场景数据可视化管理后台增加消费趋势、食堂人流量、校区消费热力图等图表用 ECharts 就能完成多校区多商户做多租户改造支持一个系统服务多个校区各校区数据隔离如果你拿这个项目做毕设能在一个核心闭环扎实的基础上再点缀一两个有亮点的扩展功能答辩时展示出来的完整度会比“功能很多但每个都很糙”高出一大截。最后再分享一个我个人的习惯。校园一卡通这类资金相关的系统我每次上线之前都会自己手动跑一遍“充值 - 消费 - 退款 - 对账”的完整链路打印出每一步的数据库余额和流水确认无误才让用户真正使用。这个习惯救过我很多次。开发过程中你可能会被各种框架和组件分散注意力但记住这条主线这个系统的一切最终都要落到“账”上账对不上其他功能做得再花哨都没有意义。