Java微信小程序的学生公寓电费管理系统设计与实现
2026/9/12 23:17:07 网站建设 项目流程

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

十年前的JSP+Servlet+Struts时代早已不适合新项目了,SSH那套配置能把人折磨疯。Spring Boot最大的价值不是技术多高深,而是“约定大于配置”省掉了大量XML配置,内嵌Tomcat让部署变成java -jar一个命令搞定。配合MyBatis-Plus,单表CRUD基本零SQL,只有关联查询和复杂统计才手写XML,开发效率能提升一倍以上。

有同学问过我用Spring Cloud行不行,我的回答是:杀鸡不用牛刀。这种校内系统单机部署完全绰绰有余,引入微服务只会增加Eureka、Feign、配置中心这些和学习主题无关的复杂度。单体应用配合Redis扛并发,撑到5000人在线没有问题。

权限控制这块我选的是Spring Security + JWT的组合。为什么不直接用Shiro?Spring 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) { List<Tariff> 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() { List<Room> 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都支持,建议11
MySQL8.0+5.7也能跑,但utf8mb4支持差点
Redis5.0+用来做缓存、分布式锁
Nginx1.18+反向代理和静态资源
Maven3.6+编译打包
Git2.x版本管理

小程序要求所有请求必须是HTTPS,而且要配置request合法域名。校园服务器一般没有公网的HTTPS证书,可以用学校的域名或者申请免费的Let's Encrypt证书。如果申请不下来,开发调试阶段可以在小程序后台开启“不校验合法域名”,但上线必须关掉。

MySQL的初始化脚本直接用spring.sql.init在项目启动时自动执行,或者用Flyway管理数据库版本。小项目用Flyway有点重了,建议直接把建表和初始化数据写成一个SQL文件,启动时用Spring Boot的schema.sqldata.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.log

systemd服务配置我写在/etc/systemd/system/electricity.service

[Unit] Description=Electricity System After=network.target mysql.service redis.service [Service] User=root WorkingDirectory=/opt/electricity-system ExecStart=/usr/local/java/bin/java -Xms512m -Xmx1024m -jar /opt/electricity-system/target/electricity-system.jar Restart=always RestartSec=10 SuccessExitStatus=143 [Install] WantedBy=multi-user.target

Nginx配置反向代理部分这样写:

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?id=101这样的接口,学生拿自己的token也能访问,直接删掉了别人的房间。

我处理这个问题的方案是:在Controller方法上加@PreAuthorize("hasRole('ADMIN')"),如果当前用户的角色不是管理员直接返回403。同时,宿管的操作范围要限制在自己负责的楼栋,不能让他改到别的楼栋的数据,这个在Service层做数据权限过滤。

@PreAuthorize("hasRole('ADMIN')") @DeleteMapping("/{id}") public Result<Void> deleteRoom(@PathVariable Long id) { roomService.deleteById(id); return Result.success(); }

如果没有引入Spring Security,至少要写个自定义注解和AOP拦截器,根据请求路径匹配权限。权限漏洞一旦被学生发现,整个系统就被“白嫖”了,这个教训一定要记牢。

7.6 模板消息与订阅消息的发送频次控制

微信的订阅消息分“一次性订阅”和“长期订阅”两种。校园类小程序申请长期订阅材料的门槛比较高,多数情况下只能用一次性订阅,用户每点一次授权只能收到一条消息。电费余额不足提醒这种场景就很尴尬:设置了这个功能,但用户上一次授权早就用完了,消息根本发不出去。

我的建议是不要过度依赖微信订阅消息。余额不足提醒除了消息推送外,首页房间卡片上用红色字体醒目显示“余额不足”,小程序每次打开都能看到;同时在充值成功页明确告知当前余额,让学生形成查看习惯。这就是典型的产品设计弥补技术限制的思路。

8. 真实项目交付中的几点心得

这个系统从需求确认到最终交付,前后迭代了三个版本。第一版只做了基础的查询和充值,宿管觉得“能用了但没有省多少事”,因为抄表仍然要手动算电费;第二版加了批量抄表和自动计费,宿管的效率才真正提起来;第三版补了阶梯电价、欠费提醒、报表导出,才算是完整覆盖了校方需求。这给我最大的体会是:管理系统好不好用,核心不是技术多炫,而是有没有真正打通用户的日常工作流。

还有一个容易忽略的点是“文档”。附带的文档说明不仅是给别人看的,也是给三个月后的自己看的。我在项目里整理了一份部署文档、一份接口文档和一份数据库设计说明,部署文档细化到每条命令、每个配置文件路径;接口文档用Swagger自动生成再补充字段说明;数据库设计说明里记录了每张表的用途和字段含义。半年后学校要求加个“导出月度用电报表”功能,我翻着文档半小时就定位到了需要改的地方,这个收益远超写文档花掉的时间。

如果后续有读者想在这个项目基础上做扩展,我比较推荐加这几个方向:一是引入大屏展示,把整个公寓的用电数据、异常告警、充值流水投到大屏幕上,宿管和后勤领导一眼能看清全局;二是对接学校的统一门户单点登录,让学生直接用学号登录,不用再走微信授权;三是做微信小程序端的管理入口,宿管不用专门开电脑管理端,手机上就能完成抄表和查看扣费记录。这几个方向难度不大,和现有架构也兼容,实际价值都很高。

项目做完了不代表结束,上线后还要持续处理各种边角问题。比如微信支付的证书过期要提前告警、MySQL慢查询日志要定期分析、学生换宿舍导致房间绑定的变更要有人维护。这些运维细节才是一个系统能长期稳定跑下去的关键。希望这篇拆解文能帮到正在做类似项目的朋友,少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询