Java微信小程序的学生公寓电费管理系统设计与实现
1. 项目概述与竞品思路拆解做校园类管理系统这么多年学生公寓电费这块一直是“看起来简单、做起来想骂人”的典型场景。为什么这么说我先说几个真实痛点宿舍电费查起来麻烦要么去楼下公告栏看手写表要么找宿管拿个本子翻充值流程原始学校财务只收现金或者现场扫码一旦对不上账就得人工核销更头疼的是电费没了直接“强制下线”学生那边毫无预警宿管这边还不断被半夜敲门。这套“Java基于微信小程序的学生公寓电费信息管理系统”就是冲着这些痛点来的。整个系统的技术栈很清晰后端用Java生态主流选择是Spring Boot 2.7.x搭配MyBatis-Plus做持久层小程序端就是微信原生框架配合Vant Weapp组件库做UI数据库用MySQL 8.0缓存和分布式锁交给Redis。说白了这个项目就是个典型的“管理端用户端”双端结构一端给宿管、辅导员、财务用一端给学生用。为什么选Java而不是PHP或者Node核心原因是学校信息中心现有的服务器环境基本都是Linux Tomcat MySQL这套组合Java部署进去不用额外装运行时而且学校系统最常见的集成需求是统一身份认证和支付对接Java体系在这方面的类库和案例最全。小程序端为什么不用uniapp而是原生微信框架因为这套系统面向的是校内固定场景用户量撑死几千人不需要多端复用原生框架包体小、启动快、调微信API最直接踩坑成本最低。从整体架构上看这个项目非常适合三类人学习和复现第一类是计算机专业做毕业设计的学生前后端分离结构完整、业务闭环、自带真实应用场景答辩时好讲故事第二类是学校信息中心的开发人员可以直接二次改造成校园缴费中台的子模块第三类是刚入行的Java开发想找一个不算复杂但五脏俱全的实战项目来补全对“权限控制、支付回调、定时任务、报表导出”这些常规企业级功能的认知。先说清楚这套系统到底解决了什么核心问题。第一把“电费数据”从宿管手抄的纸质台账挪到了数据库里电表读数录入、单价配置、按宿舍维度汇总都有据可查第二把“充值缴费”从线下挪到了线上小程序里学生微信支付后自动到账不用再揣着现金跑值班室第三把“用电状态”从“黑灯才知道欠费”变成了“余额不足先预警、触发阀值再断电”减少冲突也减少麻烦。这三件事做完宿管省心、学生省事、财务对账轻松项目价值自然就立住了。下面我从一个完整可落地的角度把整个系统的设计思路、关键代码实现、数据库表结构、部署上线流程全部拆开讲一遍。内容覆盖面比较广但每一步我都会标注“为什么这么做”而不是只丢一段代码让你自己琢磨。2. 系统整体设计与技术选型逻辑2.1 功能模块划分与业务流程梳理站在产品视角这个系统可以拆成四条主流程用户认证流程、电费查询与充值流程、电表抄录与计费流程、管理端数据处理流程。用户认证流程走的是微信生态的标准玩法小程序端调用wx.login()拿到临时code后端拿着code去微信接口换取openid用openid作为用户唯一标识而不是让用户注册账号密码。首次登录时自动创建学生记录并绑定宿舍房间号再次进入时直接跳转首页。这套方案我强烈建议所有校内系统都这么做因为学生记校内各种系统密码已经够多了能少一步是一步。电费查询与充值流程是用户端的核心。学生进入小程序首页调用后端接口拿到当前房间的剩余电量、今日用电、本月用电和电费单价。充值时分两步走第一步调用后端创建充值订单后端生成商户订单号并记录到订单表第二步调起微信支付wx.requestPayment核心参数由后端返回而不是前端拼装。支付完成后微信服务器会异步回调后端接口后端在回调里更新订单状态并给房间余额增加电量同时通过模板消息通知学生“充值到账”。管理端这边核心是电表抄录。宿管查看楼栋下每个房间的电表读数列表录入当前表显后端根据“本次读数减去上次读数”计算出用电量再乘以当前电价得到应扣金额从房间余额中扣减。这里要注意真实宿舍场景里电表不是每时每刻连着系统返数的大部分学校还是人工抄表所以“抄表-计算-扣费”这个链路必须设计得足够顺滑。2.2 为什么后端选Spring Boot为什么不选SSH十年前的JSPServletStruts时代早已不适合新项目了SSH那套配置能把人折磨疯。Spring Boot最大的价值不是技术多高深而是“约定大于配置”省掉了大量XML配置内嵌Tomcat让部署变成java -jar一个命令搞定。配合MyBatis-Plus单表CRUD基本零SQL只有关联查询和复杂统计才手写XML开发效率能提升一倍以上。有同学问过我用Spring Cloud行不行我的回答是杀鸡不用牛刀。这种校内系统单机部署完全绰绰有余引入微服务只会增加Eureka、Feign、配置中心这些和学习主题无关的复杂度。单体应用配合Redis扛并发撑到5000人在线没有问题。权限控制这块我选的是Spring Security JWT的组合。为什么不直接用ShiroSpring Security虽然上手曲线陡一点但它对OAuth2、方法级鉴权的支持更完整以后如果要对接学校统一门户的单点登录扩展起来顺手。JWT无状态认证非常适合小程序这种多端场景服务端不需要存session水平扩展时不用考虑session同步问题。2.3 小程序端原生框架为什么够用现在网上铺天盖地推uniapp多端复用确实诱人但回到这个项目本身目标用户全在微信里没有同时上支付宝小程序、抖音小程序的需求。原生小程序框架的wxml/wxss结构和HTML/CSS高度相似上手难度低而且调试工具稳定、API调用链路短出了问题查起来直接。UI组件库我建议直接用Vant Weapp它是有赞开源的那套表单组件、弹出层、消息通知、进度条都够用长得也符合学生群体的审美习惯。关键是不用自己写一堆picker、field组件开发效率提升明显。小程序的网络请求我封装在utils/request.js里统一处理baseURL、请求头里塞token、响应码拦截、错误toast这几个逻辑。这样业务代码里只需要api.getRoomInfo({ roomId: 101 }).then(res { this.setData({ roomInfo: res.data }) })后端接口数据结构统一用{ code: 200, message: success, data: {} }来包前端拦截器把res.data.data解出来直接给业务层用。这套约定在前后端分离项目里必须一开始就定好不然联调时百八十个接口各回各的格式能折腾死人。3. 数据库设计每张表都有存在的理由3.1 核心表结构说明与字段设计数据库是整个项目的地基地基歪了后面盖什么都塌。这个项目我一共设计了8张核心表用户表、学生信息表、宿舍楼栋表、房间表、电表抄录表、充值订单表、扣费记录表、电价配置表。下面挑最关键的几张讲设计思路。用户表sys_user主要存登录凭证字段围绕微信体系设计CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, session_key varchar(255) DEFAULT NULL COMMENT 会话密钥, phone varchar(20) DEFAULT NULL COMMENT 手机号, role tinyint(4) NOT NULL DEFAULT 0 COMMENT 角色 0学生 1宿管 2管理员, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态 1正常 0禁用, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;openid必须加唯一索引这是所有业务的凭证起点。role字段控制权限级别没有单独建角色表是因为校内系统角色就三类写死在枚举里反而清爽。房间表room是电费业务的载体需要注意的字段有当前余额、累计用电量、房间状态和绑定楼栋CREATE TABLE room ( id bigint(20) NOT NULL AUTO_INCREMENT, building_id bigint(20) NOT NULL COMMENT 楼栋ID, room_no varchar(20) NOT NULL COMMENT 房间号, floor int(11) DEFAULT NULL COMMENT 所在楼层, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额(元), total_usage decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 累计用电量(度), status tinyint(4) NOT NULL DEFAULT 1 COMMENT 1正常 0欠费断电, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_building (building_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房间表;balance字段存的是“钱”而不是“度”这个设计是跟电价挂钩的。缴费时直接往balance里加钱扣费时按用电量乘单价从balance里减钱。如果只存度数涉及阶梯电价时就很难处理。status字段控制通断电状态欠费时由定时任务把status置为0充值到账后再置为1。电价配置表tariff要支持阶梯电价这是很多同类项目忽略的细节字段名类型说明idbigint主键tariff_namevarchar配置名称如“学生宿舍标准电价”step_valuedecimal阶梯阈值度step_pricedecimal阶梯单价元/度effective_datedate生效日期statusint是否启用为什么要有阶梯很多学校为了鼓励节约用电每月用电超过一定度数后单价会上浮。比如基础电量内0.55元/度超出部分0.65元/度。后端每月计算费用时先查该配置表再按阶梯规则分段计算。3.2 订单与扣费流水账要算得清充值订单表charge_order是连接微信支付的核心表设计时字段不能省CREATE TABLE charge_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL COMMENT 商户订单号, room_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL COMMENT 操作人, amount decimal(10,2) NOT NULL COMMENT 充值金额(元), status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭, transaction_id varchar(64) DEFAULT NULL COMMENT 微信支付单号(回调后写入), paid_time datetime DEFAULT NULL COMMENT 支付时间, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT充值订单表;order_no必须唯一生成规则建议用“日期随机数”组合比如20250612103059001避免并发下重复。transaction_id是微信支付回调时带过来的支付单号存下来方便财务对账。status字段的三态流转非常关键下单时0回调成功后改1超时或者用户取消改2。扣费流水表usage_record记录每一次电费扣减明细相当于账本的明细页。字段包括房间ID、抄表记录ID、用电量、电价、扣减金额、扣费时间。这张表做到“每一度电都有迹可循”学生如果对电费有疑问宿管可以直接按时间区间查出明细给解释省掉大量扯皮。3.3 数据关系和索引设计要点房间表通过building_id关联楼栋表通过room_no定位具体位置学生信息表通过room_id和房间表建立归属关系抄表和扣费记录都挂在room_id上充值订单除了room_id还记录了操作人user_id。整个业务链路以“房间”为轴心所以room_id相关的索引要建到位ALTER TABLE charge_order ADD INDEX idx_room (room_id); ALTER TABLE usage_record ADD INDEX idx_room_time (room_id, create_time);两表关联查询低频的话单索引就够。但像“某个房间最近一个月的缴费记录”“按时间范围查某房间的用电明细”这类高频查询组合索引的收益非常明显。MySQL最左前缀原则大家都懂但真正建表时容易忽略我踩过这个坑补上。4. 核心接口设计与后端关键实现4.1 登录认证接口与拦截器设计微信登录的完整链路是这样的小程序端wx.login()获取code传给后端/api/auth/login后端调用https://api.weixin.qq.com/sns/jscode2session换取openid和session_key用openid查用户表有就直接生成JWT返回没有就自动注册一条用户记录。这个接口的代码不复杂但有个坑必须提code是一次性的后端调用微信接口失败后不能让前端重新调一次wx.login()所以要在后端做缓存容错。登录时查不到用户的场景也要处理好尤其是新生还没分配房间时前端要提示“请联系宿管完成房间绑定”。JWT的生成我用jjwt库把userId、role、过期时间塞进token。拦截器统一从请求头里取Authorization字段解析token判断合法性。这里有个实践细节小程序的request请求默认不会自动带header必须在封装好的请求方法里手动设置。Component public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BizException(401, 未登录或登录已过期); } // 解析token把用户信息放入ThreadLocal UserContext.set(JwtUtil.parse(token)); return true; } }ThreadLocal存用户信息这个操作很关键后面的业务方法里想拿当前登录人就直接UserContext.get().getUserId()不用在每个方法里都传一遍userId。WebMvcConfig里注册拦截器时记得排除登录接口和支付回调接口这两个接口外部访问时不可能带token。4.2 充值与微信支付回调的并发一致性充值下单接口的逻辑是接收金额、房间ID参数校验金额范围比如1-500元生成order_no保存订单初始状态调微信统一下单API拿到prepay_id再封装成小程序端wx.requestPayment需要的参数返给前端。这里要特别注意签名。微信支付每一笔请求都要用商户API密钥做HMAC-SHA256签名参数要按照字典序排序后拼接任何一个参数值为空都不能参与签名。签名出问题是最常见的新手报错调试时先把参与签名的参数和最终字符串打日志出来和微信支付平台沙箱里的对比一下基本一眼就能找出问题。支付回调是整套系统里最容易出并发bug的地方。微信服务器会在支付成功后异步POST回调到我们配置的/api/pay/notify这个接口必须做幂等处理否则同一个支付结果回调两次学生余额就会加两遍。PostMapping(/notify) public String notify(RequestBody String xmlData) { // 1. 校验签名 // 2. 解析结果取出order_no和transaction_id // 3. 查询订单如果订单状态已是1直接返回成功 // 4. 加锁扣款更新 ChargeOrder order orderService.getByOrderNo(orderNo); if (order.getStatus() 1) { return successXml(); // 幂等已处理直接返回 } // 用redis锁防止并发重复处理 String lockKey PAY_LOCK_ orderNo; if (!redisLock.tryLock(lockKey)) { return successXml(); // 没拿到锁说明别人在处理直接返回 } try { order.setStatus(1); order.setTransactionId(transactionId); orderService.updateById(order); roomService.addBalance(order.getRoomId(), order.getAmount()); } finally { redisLock.unlock(lockKey); } return successXml(); }Redis分布式锁在这里的作用是防止多个线程同时处理同一个订单避免余额加重复。虽然微信回调理论上是串行的但万一服务器重启导致消息重投锁就是最后一道防线。实测下来这个方案能撑住教务系统调课高峰期的并发量。4.3 抄表计费与阶梯电价计算抄表计费是管理端的核心业务。宿管在管理端录入当前电表读数后后端要自动完成“计算用电量→查阶梯电价→计算费用→扣减余额→更新欠费状态”这一整条链路。阶梯电价计算这块我用一个独立的方法处理方便单测。假设配置规则“当月用电100度以内0.55元/度101到200度0.65元/度200度以上0.85元/度”public BigDecimal calcFee(BigDecimal usage) { ListTariff rules tariffService.getActiveRules(); BigDecimal totalFee BigDecimal.ZERO; BigDecimal remain usage; BigDecimal prevStep BigDecimal.ZERO; for (Tariff rule : rules) { BigDecimal step rule.getStepValue(); if (remain.compareTo(step.subtract(prevStep)) 0) { totalFee totalFee.add(step.subtract(prevStep).multiply(rule.getStepPrice())); remain remain.subtract(step.subtract(prevStep)); prevStep step; } else { totalFee totalFee.add(remain.multiply(rule.getStepPrice())); remain BigDecimal.ZERO; break; } } if (remain.compareTo(BigDecimal.ZERO) 0) { totalFee totalFee.add(remain.multiply(lastPrice)); } return totalFee; }计算完成后生成扣费流水更新房间余额。如果余额变成负数立刻把房间status置为欠费断电。这里有个产品细节不会因为一次扣费就让房间断电而是给一个容错值比如余额低于-10元才断电防止抄表误差导致误伤。4.4 定时任务欠费断电与告警通知欠费断电不需要宿管手动操作用Spring Schedule跑一个定时任务每5分钟扫描一次所有房间把余额小于0的房间status置为断电状态。反过来学生充值成功时在支付回调里直接恢复供电。告警通知我做了两级余额低于10元时通过订阅消息给学生的微信发一条“电费不足提醒”余额低于0时再发一条“已欠费断电请尽快充值”。订阅消息的模板ID要到微信公众平台申请每次发送有频次限制但电费这种刚需场景用一次用户授权一次基本不会触发限制。定时任务的代码很简单Scheduled(cron 0 0/5 * * * ?) public void scanArrearsRooms() { ListRoom rooms roomService.listAll(); for (Room room : rooms) { if (room.getBalance().compareTo(BigDecimal.ZERO) 0 room.getStatus() 1) { room.setStatus(0); roomService.updateById(room); // 发送通知 notifyService.sendArrearsNotification(room); } } }要注意的是这个任务要加分布式锁。如果项目后期部署了多个实例同一个任务会被触发多次虽然只更新一次不会出大错但通知消息会重复发。用Redisson的RLock注解或者手动tryLock都能解决。5. 微信小程序端实现与页面细节5.1 页面结构与配置小程序端页面一共6个登录页其实是个授权引导页、首页显示房间用电信息和余额、充值页金额选择和支付、用电明细页历史电量记录、个人中心页个人信息、修改绑定、管理端页面宿管视角的抄表和全局管理。app.json里注册好页面路径tabBar配置首页、用电明细、个人中心三个主入口。首页顶部用房间卡片展示核心数据房间号、余额、本月用电量、当前状态。再往下是两个按钮去充值、看明细。这套布局符合“一目了然”的工具型小程序设计原则。首页数据获取我放在onShow而不是onLoad里因为每次从充值页返回时余额要有变化onLoad只在首次加载时触发一次onShow每次切回页面都会触发能天然刷新数据。onShow() { this.loadRoomInfo(); }, loadRoomInfo() { api.getRoomInfo().then(res { this.setData({ roomInfo: res, balanceText: res.balance.toFixed(2) }); }); }5.2 充值页与支付调起充值页的金额选择我用按钮组而不是输入框提供了10、20、30、50、100五档快捷金额减少输入错误。也可以加一个“自定义金额”输入框但前端要校验范围在1到500之间。支付调起是整个前端最核心的代码async handleRecharge() { const amount this.data.selectedAmount; // 1. 请求后端创建订单 const res await api.createChargeOrder({ amount, roomId: this.data.roomId }); // 2. 拉取微信支付参数 const payParams res.data; // 3. 调起微信支付 wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success: (res) { wx.showToast({ title: 充值成功, icon: success }); // 跳转回首页并刷新余额 wx.switchTab({ url: /pages/index/index }); }, fail: (err) { if (err.errMsg.includes(cancel)) { wx.showToast({ title: 已取消支付, icon: none }); } } }); }这里有个关键点支付结果不能只依赖前端回调。requestPayment的成功回调只是微信告诉小程序“支付完成”但后端不一定已经收到支付回调通知。严格来说前端shouldRefresh状态要去后端查询一下订单状态确认。我这个项目里做了一层兜底跳转首页后延迟500毫秒再查询订单状态没到账就轮询两次。注意package字段名是微信保留关键字在JavaScript里使用时候可以用payParams[package]取避免对象解构时语法报错。5.3 管理端页面抄表与全局管理管理端页面不挂在tabBar里而是从个人中心的条件入口进。后端根据当前用户角色返回不同的页面配置管理员能看到“抄表管理”“房间管理”“电价设置”“订单管理”五个区块宿管只能看到自己负责楼栋的数据。抄表页面是重点。列表按楼栋-楼层-房间排列每条数据展示房间号、上次抄表读数、本次读数输入框。宿管填完整个楼栋后点“批量提交”后端一次性校验并计算扣费。这里要给前端加提示提交后不可修改所以要确认弹窗二次校验。6. 项目部署与上线实操笔记6.1 环境准备与配置这个项目部署到学校服务器上的完整路径是开发机写代码 → Git推代码 → 服务器上Maven打包 → systemd托管Java进程 → Nginx反向代理 → HTTPS证书配置 → 小程序后台配置域名白名单。服务器环境清单组件版本说明JDK1.8或11Spring Boot 2.7都支持建议11MySQL8.05.7也能跑但utf8mb4支持差点Redis5.0用来做缓存、分布式锁Nginx1.18反向代理和静态资源Maven3.6编译打包Git2.x版本管理小程序要求所有请求必须是HTTPS而且要配置request合法域名。校园服务器一般没有公网的HTTPS证书可以用学校的域名或者申请免费的Lets Encrypt证书。如果申请不下来开发调试阶段可以在小程序后台开启“不校验合法域名”但上线必须关掉。MySQL的初始化脚本直接用spring.sql.init在项目启动时自动执行或者用Flyway管理数据库版本。小项目用Flyway有点重了建议直接把建表和初始化数据写成一个SQL文件启动时用Spring Boot的schema.sql和data.sql自动执行省事。6.2 打包部署流程记录后端打包上线时的实际流程如下cd /opt/electricity-system git pull origin master mvn clean package -DskipTests # 停掉旧服务 systemctl stop electricity # 启动新服务 systemctl start electricity # 查看日志确认启动成功 tail -50 /var/log/electricity/error.logsystemd服务配置我写在/etc/systemd/system/electricity.service[Unit] DescriptionElectricity System Afternetwork.target mysql.service redis.service [Service] Userroot WorkingDirectory/opt/electricity-system ExecStart/usr/local/java/bin/java -Xms512m -Xmx1024m -jar /opt/electricity-system/target/electricity-system.jar Restartalways RestartSec10 SuccessExitStatus143 [Install] WantedBymulti-user.targetNginx配置反向代理部分这样写server { listen 443 ssl; server_name your.domain.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }项目里配置了server.servlet.context-path/api所以小程序端请求的baseURL就是https://your.domain.com/api。顺序不能搞错先配好域名和证书再改小程序后台的合法域名不然进不了debug。6.3 上线前必须做好的检查清单上线是最容易翻车的时候我整理了一份自己项目上线前必过的检查项数据库初始化脚本在全新库上至少跑两遍不报错微信支付回调地址必须是公网能访问的HTTPS地址小程序后台的服务器域名和业务域名全部配置完毕且ICP备案状态正常后端日志级别的配置在生产环境改为INFO避免DEBUG日志刷爆磁盘检查支付宝/微信支付商户号的可用余额和提现配置用测试用户走一遍充值和退款全流程确保回调链路通。还有一个很多人忽略的申请微信支付时要选“JSAPI支付”而不是“Native支付”。JSAPI支付才能配合小程序使用Native是用于PC扫码的选错了小程序端无法调起支付。7. 常见问题与避坑经验整理7.1 微信登录“10003”错误与code过期问题小程序登录时报“invalid code”错误最典型的原因就是同一个code调用了两次。wx.login()生成的code有效期只有5分钟且只能使用一次。我在联调时遇到前端拿到code后为了调试在后端管理页面输出了好几次导致第二次请求时code已经失效。正确做法是前端把code作为一次性参数传一次就给后端后端调完微信接口立刻丢弃该code不能缓存也不能重复使用。前端联调时要看一下Network面板是不是同一个code被发出去多次。如果后端报了这个错让前端重新调一次wx.login()换新code就能解决。7.2 微信支付回调乱码与XML解析问题微信支付的结果通知是XML格式且编码为UTF-8但校园服务器上如果Tomcat的字符编码没配置好解析时会出现中文乱码。其实业务代码里基本不解析中文所以表现不明显但验签时如果用错了字符集导致签名计算错误整个回调就废了。建议统一在回调方法入参处标注RequestBody String xmlData然后转成WxPayNotifyResult对象时强制StandardCharsets.UTF_8。签名验证必须用原始收到的字符串不能用转成对象后再序列化的字符串不然签名永远对不上。这个坑我调试了整整一个下午才找到原因。7.3 余额并发扣减导致的数据不一致电费系统看起来并发量不高但高峰期比如月底结算前后宿管批量抄表和学生批量充值同时发生时同一个房间的余额更新就可能出现并发问题。如果代码写的是“先查余额再加充值金额再写回去”两个线程同时操作时可能丢更新。解决方案有两个层面数据库层面用UPDATE room SET balance balance #{amount} WHERE id #{roomId}原子SQL就不存在读改写的问题如果业务需要先读余额做判断比如“余额不足5元不能提现”那就加SELECT ... FOR UPDATE行锁。我这套系统里充值和扣费都用的原子SQL判断余额的时候用Redis缓存值兜底没出现过大问题。7.4 小程序包体超限与图片资源优化原生小程序默认包体限制2MB超过后无法上传。这个系统本身代码量不大但如果不注意引入一些大的图标库、图片素材或者fonts字体包很容易就突破了。经验做法是图片资源全部走CDN或后端接口返回不打包进小程序页面里用到的icon尽量用iconfont的字体文件而不是图片图片压缩到webp格式再上传到OSS或COS。如果实在有大的文件必须进包可以拆分成分包加载主包只放tabBar页面其他页面全放到分包里。7.5 新手最容易忽略的权限漏洞很多类似的毕业设计项目后端接口没有做细粒度的权限校验管理端的接口只要知道URL就能随便调用。比如/api/admin/room/delete?id101这样的接口学生拿自己的token也能访问直接删掉了别人的房间。我处理这个问题的方案是在Controller方法上加PreAuthorize(hasRole(ADMIN))如果当前用户的角色不是管理员直接返回403。同时宿管的操作范围要限制在自己负责的楼栋不能让他改到别的楼栋的数据这个在Service层做数据权限过滤。PreAuthorize(hasRole(ADMIN)) DeleteMapping(/{id}) public ResultVoid deleteRoom(PathVariable Long id) { roomService.deleteById(id); return Result.success(); }如果没有引入Spring Security至少要写个自定义注解和AOP拦截器根据请求路径匹配权限。权限漏洞一旦被学生发现整个系统就被“白嫖”了这个教训一定要记牢。7.6 模板消息与订阅消息的发送频次控制微信的订阅消息分“一次性订阅”和“长期订阅”两种。校园类小程序申请长期订阅材料的门槛比较高多数情况下只能用一次性订阅用户每点一次授权只能收到一条消息。电费余额不足提醒这种场景就很尴尬设置了这个功能但用户上一次授权早就用完了消息根本发不出去。我的建议是不要过度依赖微信订阅消息。余额不足提醒除了消息推送外首页房间卡片上用红色字体醒目显示“余额不足”小程序每次打开都能看到同时在充值成功页明确告知当前余额让学生形成查看习惯。这就是典型的产品设计弥补技术限制的思路。8. 真实项目交付中的几点心得这个系统从需求确认到最终交付前后迭代了三个版本。第一版只做了基础的查询和充值宿管觉得“能用了但没有省多少事”因为抄表仍然要手动算电费第二版加了批量抄表和自动计费宿管的效率才真正提起来第三版补了阶梯电价、欠费提醒、报表导出才算是完整覆盖了校方需求。这给我最大的体会是管理系统好不好用核心不是技术多炫而是有没有真正打通用户的日常工作流。还有一个容易忽略的点是“文档”。附带的文档说明不仅是给别人看的也是给三个月后的自己看的。我在项目里整理了一份部署文档、一份接口文档和一份数据库设计说明部署文档细化到每条命令、每个配置文件路径接口文档用Swagger自动生成再补充字段说明数据库设计说明里记录了每张表的用途和字段含义。半年后学校要求加个“导出月度用电报表”功能我翻着文档半小时就定位到了需要改的地方这个收益远超写文档花掉的时间。如果后续有读者想在这个项目基础上做扩展我比较推荐加这几个方向一是引入大屏展示把整个公寓的用电数据、异常告警、充值流水投到大屏幕上宿管和后勤领导一眼能看清全局二是对接学校的统一门户单点登录让学生直接用学号登录不用再走微信授权三是做微信小程序端的管理入口宿管不用专门开电脑管理端手机上就能完成抄表和查看扣费记录。这几个方向难度不大和现有架构也兼容实际价值都很高。项目做完了不代表结束上线后还要持续处理各种边角问题。比如微信支付的证书过期要提前告警、MySQL慢查询日志要定期分析、学生换宿舍导致房间绑定的变更要有人维护。这些运维细节才是一个系统能长期稳定跑下去的关键。希望这篇拆解文能帮到正在做类似项目的朋友少走点弯路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →