小程序 + 安卓 + SpringBoot,这套校内民生社团报名商城系统我复盘完了
每年毕业季都会有一批人来做“校园生活服务平台”这类题目,今年我把它升级成了一个更完整的东西:小程序端 + 安卓APP端 + SpringBoot后端,覆盖社团报名、校内商城、民生报修三个大板块。这套系统做完,基本把一个互联网产品的标准链路都走了一遍——小程序用户登录、安卓端适配、后端接口设计、并发控制、部署上线、证书配置。本文就把这次完整的开发过程拆开,每一块的设计思路和踩坑点都摊开讲,给正在做类似项目的同学留一份能直接抄作业的参考。
整个系统服务的是三类人:学生、社团管理员、后勤管理员。学生用小程序或安卓APP发起报名、下单、报修;社团管理员在后台管理社团资料和成员;后勤管理员处理报修单。前后端所有操作都走同一套SpringBoot接口,双端共用一套业务逻辑,这是性价比最高的做法。
1. 项目整体设计与技术选型,为什么是这三件套
先说结论:这个项目适合 SpringBoot 3.x + MyBatis Plus + MySQL,前端小程序选原生微信小程序,安卓端选原生 Java/Kotlin 或者套壳WebView,不建议一上来就上太重的微服务架构。
1.1 为什么SpringBoot是这套系统的最后答案
做校园类项目,SpringBoot是毫无疑问的主流选择。原因不复杂:它有自动配置机制,内嵌Tomcat,打包就是一个可直接执行的Jar包,对单人开发或者小团队协作都非常友好。比起SSH(Spring MVC + Spring + Hibernate)那套老古董,SpringBoot省掉了一大堆XML配置,开发效率高了一个量级。
具体到版本上,我这次用了SpringBoot 3.2.5,对应的JDK版本是17。这里要提醒一句,很多人还在用JDK 8,如果直接上SpringBoot 3.x是会报错的,因为SpringBoot 3基于Jakarta EE 9,包名从javax换成了jakarta,很多老代码需要改import。如果你对JDK 17不熟,建议保守一点用SpringBoot 2.7系列 + JDK 8,稳定性也有保障,不要为了追新给自己挖坑。
持久层选了MyBatis Plus,因为它对单表CRUD的支持几乎是无脑的。BaseMapper内置了insert、selectById、updateById这些基础方法,像社团列表的分页查询、用户信息的简单增删改查,不需要手写SQL。但注意MyBatis Plus的版本也要匹配SpringBoot 3,要用mybatis-plus-spring-boot3-starter,这是很多人的第一个坑。
1.2 数据库和缓存设计,为什么加Redis
数据库用MySQL 8.0,存储用户、社团、报名记录、商品、订单、报修单这些核心数据。Redis的引入主要解决两个问题:第一个是微信登录凭证的临时存储,code换session_key之后需要缓存,不缓存的话每次都要请求微信服务器,容易被限流;第二个是热点数据的缓存,比如社团列表、商品列表这类高频读取的数据。
模块划分上,我把整个系统分成五块:用户中心、社团管理、报名系统、校内商城、民生服务。每块之间的数据尽量解耦,通过userId做关联,不要用大而全的user表把什么字段都塞进去。
表结构建议单独建这几张:user(用户)、club(社团)、club_member(社团成员)、activity(活动/招新)、registration(报名记录)、product(商品)、orders(订单)、repair(报修单)。报名记录和订单是核心表,要重点设计索引。
1.3 双端共用一套后端API,接口怎么设计才不会乱
小程序和安卓APP共用同一套SpringBoot接口,所以接口规范必须从第一天就定好。我采用的是RESTful风格,统一以/api/v1开头,按资源分目录:/api/v1/user、/api/v1/club、/api/v1/shop等。返回格式统一用Result对象包装,包含code、message、data三个字段,前端拿到之后先判断code是否为200,再做业务处理。
对于登录态的识别,小程序和安卓端需要同一套认证方案。这里我用的是JWT + Redis双保险:用户登录成功后,后端签发一个JWT token,同时把token存到Redis里,设置7天过期。前端每次请求在header里带上Authorization: Bearer <token>,后端通过拦截器统一校验。为什么还要Redis存一份?因为后端可以主动让token失效,比如用户修改密码或者管理员封号,直接删Redis记录就能立即生效,这个需求JWT本身做不到。
2. 后端核心模块实现,这些细节决定了系统能不能跑稳
后端不是把CRUD写完就完事,关键的几个业务场景——微信登录、社团报名、商城订单——里面的并发控制和事务处理,才是拉开项目质量差距的地方。
2.1 微信小程序登录全流程,code换openid的完整链路
小程序端调用wx.login()拿到一个临时code,这个code有效期只有5分钟,且只能使用一次。后端拿到code后,需要通过jscode2session接口去微信服务器交换openid和session_key,这一步必须是后端做的,不能在前端完成,否则AppSecret就暴露了。
很多人在这个环节报获取登录后的微信用户失败,十有八九是AppID和AppSecret配置不对,或者后台没有开启“小程序登录”权限。调试时候有几种验证方式:先检查code是否为空,再确认后端请求微信接口的URL参数拼到了没有,常见错误是grant_type漏写或者遗漏了js_code字段。
拿到openid之后,后端会去user表查一下这个openid是否已存在,不存在就自动注册一个账户,存在就直接登录。然后生成JWT token返回给小程序,后续所有请求都靠这个token来识别用户身份。安卓端不依赖微信登录,直接用手机号+密码或者短信验证码登录,两个端的用户都要落在一个user表里,通过login_type字段区分来源。
2.2 社团报名并发控制,怎么防止一个名额被抢两次
社团招新经常会出现一个热门社团有多少个报名名额,抢的人可能过百的情况。如果只用数据库的“先查再插”,在高并发下一定会超卖。比如有两个请求同时读到剩余名额为1,然后同时执行insert,最后报名记录多了一条,名额变成负数。
解决这个问题有几种方案,从简单到复杂排:第一种是数据库乐观锁,在club表加一个version字段,更新时检查version是否匹配;第二种是业务上增加唯一约束,比如在registration表上建立(user_id, activity_id)唯一索引,这样同一个用户不可能重复报名同一个活动;第三种是Redis分布式锁,抢名额之前先尝试加锁,拿到锁才允许操作。
我实际用的是索引 + 分布式锁的组合。唯一索引兜底,防止同一个人重复报名;Redis锁控制并发,让同一时刻只有一个请求在扣减名额。注意锁的key要设计好,比如lock:activity:3,粒度要细到具体活动,不要用一把锁锁住所有活动,否则性能大打折扣。锁的过期时间要设合理,一般3~5秒就够了,业务高峰期最多也就几百毫秒就执行完。如果担心锁执行完之前就自动过期,可以用Redisson的看门狗机制,不过对于这个项目级别来说,固定过期时间已经完全够用。
2.3 校内商城的下单流程和库存扣减设计
商城模块是整个系统里事务最复杂的地方。下单不只是插入一条订单记录,还涉及库存校验、扣减、订单状态流转,每一步都不能出错。
我的做法是在Service层用@Transactional注解包裹整个下单逻辑。流程是:校验商品是否上架 -> 校验库存是否充足 -> 扣减库存 -> 创建订单 -> 返回订单号。这里有个关键点:扣减库存的SQL要用原子操作,不能先查再update:
sql复制UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0
为什么不先select再update?因为在高并发下,select出来的stock可能是过期的,update的时候会导致库存变成负数。通过update语句里带stock > 0条件,如果更新影响行数为0,说明库存不足,直接抛异常回滚。这种方式叫乐观锁的变种,比加悲观锁的效率高很多。
订单状态我设计成四个:待支付、已支付、已发货/待自提、已完成。校园场景下建议加一个自提码,学生下单后生成二维码,凭码到校内自提点取货。自提码的生成方式很简单,用UUID取前8位转大写,存入订单表的pickup_code字段。这样学生不用等快递,又省了物流对接的复杂度。
2.4 民生服务模块的通用设计,报修和意见反馈不再堆垃圾代码
民生服务这块,很多毕业设计会做得很粗糙,报修和意见反馈、失物招领往往各写一套接口,代码大量重复。我这里的做法是抽象一个通用的ServiceTicket流程:工单类型(报修/投诉/建议/失物招领)、标题、详情、附件图片、状态、紧急程度、处理人、处理结果。所有民生服务统一走这张表,只不过type字段不同。
这样做的好处是后端只需要维护一套CRUD接口,前端根据type字段展示不同的表单和列表。工单的状态机也很简单:待处理 -> 处理中 -> 已完成 -> 已关闭。管理员超时未处理的,系统可以自动置为“已加急”,配合定时任务给管理员发通知。
至于附件图片的上传,不要直接保存base64到数据库,那是灾难。我用的方案是本地目录存储,Nginx配置静态资源映射,上传时用UUID重命名文件,数据库只存相对路径。生产环境如果有条件接OSS,把存储层换掉就行,接口不变。
3. 前端双端实现记录,小程序和安卓APP的开发要点
前端是这个项目的门面,小程序和安卓APP要同时覆盖到,但两者的开发范式和侧重点差异很大,这里把两端的架构、页面组织、调试技巧分开讲。
3.1 微信小程序端,原生开发还是uni-app
做小程序有两种主流方案:原生微信小程序和uni-app。这个项目我选了原生,原因很简单:项目不涉及跨多端发布的需求,小程序只在微信生态里跑,原生框架的性能和调试体验是最好的。等你真要上支付宝小程序、抖音小程序,再用uni-app重写也不迟。
小程序端的目录结构按业务模块分页面:pages/index(首页),pages/club(社团列表),pages/club-detail(社团详情),pages/register(报名),pages/shop(商城),pages/product-detail(商品详情),pages/order(订单列表),pages/report(民生报修),pages/user(个人中心)。
有一个重点是tabBar只能配2到5个页面,我配置了首页、社团、商城、我的四个。民生报修入口放在首页金刚区,不要单独占tab。每个页面的onLoad里先检查token是否存在,不存在就跳到登录页。登录页用wx.login()静默登录,不需要用户手动授权,这是小程序体验好于H5的重要一点。
小程序端的痛点之一就是小程序头部标题和导航栏高度在不同机型上不一致。它的胶囊按钮是固定的,但状态栏高度会因机型变化。处理方法是获取wx.getWindowInfo()里的statusBarHeight,再加上胶囊按钮高度,动态计算顶部占位。前端适配要照顾到iPhone X的刘海屏和安卓各家厂商的挖孔屏,所以不要写死高度,全部用系统参数计算。
3.2 安卓APP端,原生开发和双端联调怎么做
安卓端如果从零开始写原生,工作量会大很多。这个项目采用了“原生框架 + WebView承载主要业务页面”的混合方案:用原生Java/Kotlin搭建了外壳,包括启动页、导航栏、WebView容器、消息推送的基础能力,所有业务页面用HTML5页面承载,本质上是把Web页包了一层原生壳。
这样设计的原因很直接:小程序和安卓APP共用一套后端API,Web页面可以直接复用接口和逻辑,不需要各写一套复杂交互。安卓端只需要解决WebView的缓存清理、下拉刷新、JSBridge交互(比如H5调原生方法获取定位、调起相机拍照)、Token注入这几个问题。
实际工作里尤其要注意WebView的Cookie和Token管理。我们的做法是,APP启动时从本地存储读取Token,通过evaluateJavascript注入给Web前端,Web前端每次请求把它带在header里。如果Token过期,需要触发H5跳转登录页重新登录。不建议把Token直接写在WebView的Cookie里,会被第三方统计SDK意外读走,有安全隐患。
安卓原生模块里最值得写的是二维码生成和扫描。商城自提码的展示和核销、社团现场报名扫码,都用到了Zxing库。Zxing集成起来不算复杂,依赖com.google.zxing:core,然后用QRCodeWriter生成Bitmap,扫码页面用CameraSource预览,识别后回调。核心代码:
java复制QRCodeWriter writer = new QRCodeWriter();
BitMatrix matrix = writer.encode(content, BarcodeFormat.QR_CODE, 400, 400);
Bitmap bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565);
for (int x = 0; x < 400; x++) {
for (int y = 0; y < 400; y++) {
bitmap.setPixel(x, y, matrix.get(x, y) ? Color.BLACK : Color.WHITE);
}
}
如果你对原生开发不太熟,也有一类办法是找现成的WebView框架改造。不过就算用混合开发,原生壳还是得会一点,AndroidManifest配置、启动页跳转、权限申请都没法绕开。
3.3 双端缓存与状态管理,少让用户等加载
双端的缓存策略要统一思想:列表页优先读本地缓存,再静默请求最新数据并替换,这样用户打开APP或者小程序的时候体感是即时的。小程序端用wx.setStorageSync存取关键数据,安卓WebView端可以用localStorage,两者共通点是都不建议存敏感信息。
购物车的状态管理要注意跨端一致性。用户可能在小程序上加了购物车,又在安卓APP上查看,所以购物车数据必须存在后端Redis里。Redis的Hash结构很适合做这个:key = cart:userId,field是商品id,value是数量。每次接口请求都从Redis拉最新数据,双端自然保持一致。会话过期时间设置成7天,和登录态对齐。
列表分页是小程序的老大难。WXML的wx:for不能一次渲染几千条,必须做分页。我封装了一个loadMoreMixin,监听页面触底,自动拉取下一页数据,加载中显示loading状态,没有更多数据时显示“已经到底了”,这个细节虽然小,但对体验影响非常大。
4. 部署上线与高频问题排查实录
程序写完只是第一步,部署上线才是对技术能力的真正检验。这一部分把从本机到服务器的部署流程、以及开发中高频遇到的坑都记下来。
4.1 服务器环境怎么搭,Docker部署还是宝塔面板
服务器选择上,学生项目买一台2核4G的云服务器就够了,新用户往往有优惠,99块钱一年那种完全能跑。系统装CentOS 7或Ubuntu 22.04都可以,关键是内存规划好:MySQL占500M,Redis占200M,SpringBoot默认堆内存512M,2G内存的机器会有点紧,所以要么选4G,要么把JVM参数调小。
部署方式推荐Docker Compose,一条命令把所有中间件拉起来,也方便以后迁移。不做Docker直接用宝塔面板也可以,适合不熟悉Linux命令的人。我这次用的是宝塔,原因不是说它比Docker好,而是它自带MySQL、Redis、Nginx的可视化管理,文件的配置和日志查看都直观很多,对新手友好。
注意几个要改的默认配置:MySQL的root密码别设得太简单,Redis必须设置密码并注释掉protected-mode yes,SpringBoot的application.yml连接信息不要用明文,可以放到环境变量里。但这些对毕设项目来说见仁见智——如果只是想跑通展示,application.yml写死也能接受;如果你是工作项目,这个问题会被安全扫描直接拉黑。
4.2 HTTPS证书配置与小程序合法域名校验
微信小程序有一个硬性要求:所有请求接口必须是HTTPS域名,而且域名需要在小程序后台配置为合法域名。很多人在这里卡住,因为本地开发用http://localhost:8080没问题,一上线全部被拦截。
解决步骤是:先用自己的域名解析到服务器IP,然后在Nginx里配置SSL证书。证书用免费的就行,申请周期一般几分钟就能拿到。Nginx关键配置:
bash复制server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
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;
}
}
然后在微信公众平台的“开发管理-服务器域名”里把https://api.example.com加进request合法域名。安卓APP没有这个限制,WebView可以访问任意HTTPS地址甚至HTTP地址,调试期很方便,但如果上架应用市场,要求也必须是HTTPS,所以统一走HTTPS是最省心的。
4.3 高频异常排查速查表,这些问题我基本都遇到过
开发过程中踩过的坑,整理成一张速查表,遇到同类问题直接对照处理。
| 异常现象 | 可能原因 | 排查方法 |
|---|---|---|
| 小程序登录报 wx1cb4398 开头的错误 | 多半是code过期、AppSecret不对 | 用微信开发者工具查看后台请求日志,检查后端日志是否有报错堆栈 |
| 接口请求报401 | Token过期或未携带 | 检查前端请求拦截器,确认是否在header里带上了Authorization |
| 安卓WebView白屏 | Token注入失败或H5页面JS报错 | 用adb工具抓日志,看WebViewChromeClient里的onReceivedError回调 |
| 报名人数超过名额 | 并发控制失效 | 检查是否加了唯一索引和Redis锁,可以先压测复现 |
| 图片上传后无法访问 | Nginx静态资源路径配置错误 | 检查nginx的root路径和图片相对路径的拼接逻辑 |
| 小程序无法请求域名 | HTTPS证书过期或不在合法域名列表 | 在微信开发者工具里点“详情-域名信息”查看具体拦截原因 |
| Redis连接失败 | 未设置密码或端口未开放 | 本地用redis-cli ping测连通性,云服务器检查安全组放行6379端口 |
其中最有价值的一条经验是:遇到前端报错,先从后端日志入手,而不是盯着前端控制台猜。SpringBoot的日志会记录请求路径、状态码、异常堆栈,很多时候问题一目了然。我习惯在Controller的入口打印一条简短日志,记录请求时间、接口名、入参,在异常处理器里打印完整堆栈,排除问题会快很多。
4.4 上线前必须检查的几个细节
上线前逐项对照检查,不要等到了答辩现场才出问题。
第一,小程序appid换成正式的。很多人在开发者工具里用测试号开发,上线前忘了切换,导致登录失败。第二,后端数据库的用户名密码、Redis地址不要遗留测试环境配置,用spring.profiles.active=prod区分环境,明文密码放到application-prod.yml里并加gitignore忽略掉。第三,支付宝/微信支付申请不下来怎么办?校园商城场景可以做成虚拟支付+线下收款,订单状态允许管理员手动确认收款,这样不需要企业资质也能完成闭环。第四,所有接口添加统一的错误码对照表,前端判断code时不要用魔法数字,封装成一个常量文件。第五,如果展示时候可能会被问到“这个项目有什么亮点”,建议把“双端统一登录态”“报名防并发超卖”“商城库存原子扣减”这三点作为核心亮点写进PPT里,这些细节正是区分普通CRUD项目和完整工程项目的分水岭。
5. 项目复盘,哪些地方值得再完善
把整个项目做完,回头看最有价值的部分,不是用了什么新技术,而是通过一个完整业务场景,把用户端和管理端的闭环梳理清楚,并保证系统在高并发场景下基本不崩。这一条链路走下来,对SpringBoot的核心生命周期、事务控制、缓存使用、接口设计、Nginx部署都有了实操层面的理解。
如果再给我一段时间去完善,我会优先做三件事:把报修工单的定时提醒做起来,用Spring Schedule每天扫描超时未处理的工单,推送给管理员;给商城模块接上真实的支付渠道(微信支付很容易在校园场景被调用);把安卓端从WebView混合开发逐步替换成原生页面,尤其是扫码和消息推送这部分,原生体验还是明显更顺滑的。
做完项目的最大体会是,一个系统重点不在功能多,而在每个功能是不是真的能用、能扛住真实的使用场景。比如报名并发这件事,如果只是做个单机demo,永远发现不了问题;但一旦放到答辩现场有一百个人同时点报名,那个只写CRUD的系统瞬间就能暴露短板。希望这篇复盘能帮你少走弯路,尤其在登录态、并发控制和HTTPS配置这三个地方提前做准备,你会发现整个开发节奏会顺很多。
