每年这个季节,后台总会涌入大量关于毕业设计的问题。今年问得最多的,不是“怎么做”,而是“我能用它做什么”。我接到的这个项目比较典型——基于Android的高校校史展厅管理小程序设计与实现,班里同学拿它当毕设,从提交源码、配套文档到远程调试、一对一讲解、后期定制,一套流程下来基本能完整覆盖毕设的全部环节。今天把这套东西从设计思路到落地细节完整拆一遍,给正在选题或者卡在实现阶段的同学做个参考。
这个项目本身解决的是高校校史展厅的管理问题:平时展厅里的展品信息、预约参观、导览讲解,靠人工管理既费时又容易出错,而多数高校已经有很多历史资料积淀,但缺乏一个轻量化的展示和管理入口。小程序端(微信小程序)负责面向访客和展厅管理员的交互,Android端承担部分管理端功能与后台服务工作,整体构成“前端展示-服务交互-数据管理”的闭环。说白了,就是既能让访客扫码看展品、预约、听讲解,又能让管理员在后台维护展品、审核预约、查看数据的一个系统。
这部分内容适合谁来看?如果你正在做Android或小程序相关的毕业设计,需要一套能讲清楚“为什么这么设计”、能应付答辩追问的项目;或者你想在校史馆这类校内场景做一个实际可用的管理系统,这篇拆解可以直接抄作业。我会把选型逻辑、功能拆解、代码实现、调试避坑、答辩要点全部过一遍,内容偏实操,尽量少讲虚的。
1. 项目整体设计思路与技术选型
1.1 核心需求解析:校史展厅到底需要什么
不用一上来就写代码,先搞清楚场景。高校校史展厅一般有这几类角色:普通访客(学生、校友、来宾)、展厅管理员(老师或学生助理)、系统超级管理员。平时的工作场景是:访客要预约参观、进入后希望看到展品的详细介绍、想按年代或主题了解校史;管理员要登记展品信息、维护展厅公告、审核预约、查看每日参观量;学院领导或负责老师需要能够直观地看到展厅运营情况。
所以需求可以拆成三层:
- 访客端(小程序):校史展厅介绍、展品分类浏览、展品详情查看、参观预约、在线留言反馈、个人预约记录。
- 管理端(Android + 小程序内嵌管理页):展品信息维护(增删改查)、展厅公告发布、预约审核、参观数据统计、留言管理。
- 支撑层:用户身份认证、图片存储、预约时段管理、数据报表基础统计。
这个需求模型基本可以应付大多数校园场景的管理类毕设题目。如果你的题目是博物馆、图书馆、实验室展厅,结构完全相通,改改文案即可。
1.2 为什么是小程序 + Android 组合,而不是纯App
很多同学会问:直接用Android写个App不行吗?为什么还要挂一个小程序?
我当时的考虑是这样的:校史展厅的使用场景是“访客到达 → 扫码/搜索打开 → 浏览展品 → 预约下次参观”,如果要求访客现场下载一个App,门槛太高,推广成本大。小程序的“扫一扫即用”、“无需安装”正好契合这个场景。管理员端日常操作频率高、需要处理图片和较多的数据维护,做成Android原生应用体验更顺手,还能作为毕设展示原生开发能力。两者共用一个后端数据服务,前端展示和管理操作分离,逻辑清晰,一问就能答上来。
还有一个非常现实的理由是:毕设评审看重的是“完整度”和“技术覆盖面”。你同时用到了小程序开发、Android开发、移动端与服务端的数据交互,覆盖面广,答辩时能有充分的展示话题。纯Android App虽然也能过,但相对单薄。
1.3 技术选型:从后端到前端的组合方案
这个项目的技术栈,我建议按“后端服务 + Android管理端 + 微信小程序端”三块来选。
- 后端服务:Spring Boot + MyBatis Plus + MySQL。不选Node.js是因为Spring Boot在校园环境里更常见,资料多,遇到问题也好搜。如果你对PHP更熟悉,用ThinkPHP或Laravel也可以,但要注意接口规范保持一致。
- Android管理端:原生Java/Kotlin + OkHttp + RecyclerView。界面用Material Design风格,图片加载用Glide。这里不推荐用WebView套壳,否则答辩时容易被问“你的Android原生体现在哪里”。
- 小程序端:原生微信小程序(WXML/WXSS/JS),不推荐直接上UniApp。虽然UniApp开发效率高,但这是毕设,原生小程序更利于展示你对框架本身的理解,而且原生小程序的调试、审核、自动化测试在毕设这种体量下完全够用。
- 数据交互:RESTful API + JSON;文件存储用服务器本地目录即可,没必要上OSS,但注意上传路径配置。
2. 功能模块拆解与数据库设计
2.1 功能模块划分与页面流转
整个系统按角色分模块,画结构如下:
访客小程序端
- 首页:展厅轮播图、公告列表、快捷入口(预约、导览、留言)
- 展品展示:分类标签(建校历程、校园风貌、名师风采、荣誉成就)、展品列表、展品详情
- 参观预约:选择日期、时段、参观人数,填写联系方式,提交后等待审核
- 个人中心:我的预约、我的留言、展厅介绍
Android管理端
- 登录:管理员账号密码认证
- 仪表盘:今日预约数、待审核预约、累计访客量、展品总数
- 展品管理:展品列表、新增/编辑/删除、图片上传
- 预约审核:待审核列表、通过/拒绝
- 公告管理:公告列表、发布
- 留言管理:查看、删除违规留言
公共支撑
- 用户角色鉴权(JWT或简单Token)
- 统一返回格式(code, msg, data)
- 图片上传接口
2.2 数据库表设计:不要漏字段
表结构是整个系统能不能跑通的关键。我最终设计了6张核心表,这是经过反复调整后的“最小可运行版本”:
- user表:id、username、password、role(admin/user)、avatar、phone、create_time
- exhibit表:id、title、cover_img、images(多图逗号分隔)、category、content、era_year、create_time
- appointment表:id、user_id、date、time_slot、visitor_count、phone、status(pending/approved/rejected)、remark、create_time
- notice表:id、title、content、cover_img、create_time
- message表:id、user_id、content、reply、create_time
- banner表:id、img_url、link_url、sort_order、create_time
需要注意的几点:
exhibit.images用逗号分隔保存多图是最省事的,解析和存储成本都低,不是大数据量场景没必要单独建附表。appointment.time_slot建议用字符串存,如"09:00-11:00",而不是存时间戳,因为展馆时段往往是固定枚举,存字符串展示更直接。- 每个表都保留
create_time,统计报表时会用上。 - 用户表要区分
role,管理员和普通访客共用一个表,鉴权时通过角色拦截。
2.3 接口设计规范与权限控制
前后端分离后,接口规范直接影响联调效率。我统一采用如下风格:
GET /api/exhibit/list展品列表GET /api/exhibit/detail?id=1展品详情POST /api/appointment/add新增预约GET /api/appointment/myList?userId=1我的预约POST /api/admin/login管理员登录GET /api/admin/overview仪表盘数据POST /api/admin/exhibit/save新增/编辑展品POST /api/admin/exhibit/delete删除展品POST /api/admin/appointment/audit预约审核
权限控制,我使用的是简单Token方案:用户登录后返回一个token,存到本地(小程序存storage,Android存SharedPreferences),后续请求在Header里带Authorization: token。后端用拦截器校验token,同时校验角色。不用Spring Security,因为毕设体量下配置复杂度大于收益,手写拦截器足够,且答辩时可以讲清楚每个环节。
3. 核心环节实现:从接口到页面的关键代码
3.1 后端接口实现:以展品列表为例
后端我用的Spring Boot,Controller层保持薄,业务逻辑放Service。这里给一个展品分类列表的实现示例,注意不是伪代码,是可以直接落地的写法:
@RestController @RequestMapping("/api/exhibit") public class ExhibitController { @Autowired private ExhibitService exhibitService; @GetMapping("/list") public Result list(@RequestParam(required = false) String category, @RequestParam(required = false) String keyword) { List<Exhibit> list = exhibitService.getList(category, keyword); return Result.success(list); } @GetMapping("/detail") public Result detail(@RequestParam Integer id) { Exhibit exhibit = exhibitService.getById(id); if (exhibit == null) { return Result.error("展品不存在"); } return Result.success(exhibit); } }对应的Service实现:
@Service public class ExhibitServiceImpl implements ExhibitService { @Autowired private ExhibitMapper exhibitMapper; @Override public List<Exhibit> getList(String category, String keyword) { QueryWrapper<Exhibit> wrapper = new QueryWrapper<>(); if (StringUtils.hasText(category)) { wrapper.eq("category", category); } if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like("title", keyword) .or().like("content", keyword)); } wrapper.orderByDesc("create_time"); return exhibitMapper.selectList(wrapper); } }返回结构统一使用Result包装类,包含code、msg、data三个字段。这样小程序端和Android端共用一个解析规则,不会出现“你返回数组,我解析对象”的典型对接冲突。
需要特别强调一个细节:QueryWrapper中多条件查询时,and嵌套要注意括号层级。如果直接写wrapper.like("title", keyword).or().like("content", keyword),在拼接SQL时,如果前面还有eq条件,逻辑会变成category = ? and title like ? or content like ?,因为and和or优先级问题导致查询结果和预期不符。上面代码里用.and(w -> w.like(...).or().like(...))包了一层,生成的SQL就是category = ? and (title like ? or content like ?),意思就对了。这个坑我调试了将近两个小时,写出来给你们避雷。
3.2 小程序端核心页面:展品分类与详情
小程序端我是纯原生写的,没有引入任何第三方UI库。自定义组件这一块需要动手实现,因为小程序原生组件库在UI美观度上确实不如第三方库,但如果直接引入Vant Weapp,又要多学习一套组件API。考虑到毕设需要展示“原生”能力,我用自定义组件方式做了一套简单的卡片列表。
展品列表页关键逻辑:
Page({ data: { categoryList: ['全部', '建校历程', '校园风貌', '名师风采', '荣誉成就'], activeCategory: '全部', exhibitList: [], loading: false }, onLoad() { this.fetchList(); }, switchCategory(e) { const category = e.currentTarget.dataset.category; if (category === this.data.activeCategory) return; this.setData({ activeCategory: category }); this.fetchList(); }, fetchList() { const { activeCategory } = this.data; const category = activeCategory === '全部' ? '' : activeCategory; this.setData({ loading: true }); wx.request({ url: `${BASE_URL}/api/exhibit/list`, data: { category }, success: (res) => { if (res.data.code === 200) { this.setData({ exhibitList: res.data.data }); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); } }, complete: () => { this.setData({ loading: false }); } }); } })这里有一个关键点,wx.request的success回调里this指向问题。在Page中定义的方法里直接用函数,而不是箭头函数时,this会指到全局或undefined。所以这里使用了普通函数,配合success回调内部直接使用外层this的ES6箭头函数?注意,上面代码里success: (res) => {}使用的是箭头函数,箭头函数不绑定自己的this,会继承外层Page作用域的this,所以能正常setData。如果你写成success: function(res) { this.setData(...) },这里的this就是undefined了,必须提前const that = this才能用。这个区别在联调时经常出错,很多同学卡在这一步。
3.3 Android管理端:RecyclerView列表与图片上传
Android管理端最核心的功能是展品列表的展示和新增/编辑。使用 RecyclerView + 自定义Adapter实现列表展示,图片加载用 Glide。
class ExhibitAdapter( private val exhibitList: MutableList<Exhibit>, private val onClick: (Exhibit) -> Unit ) : RecyclerView.Adapter<ExhibitAdapter.ViewHolder>() { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val titleTv: TextView = itemView.findViewById(R.id.tv_title) val categoryTv: TextView = itemView.findViewById(R.id.tv_category) val coverIv: ImageView = itemView.findViewById(R.id.iv_cover) val editBtn: Button = itemView.findViewById(R.id.btn_edit) val deleteBtn: Button = itemView.findViewById(R.id.btn_delete) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_exhibit, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val exhibit = exhibitList[position] holder.titleTv.text = exhibit.title holder.categoryTv.text = exhibit.category Glide.with(holder.itemView.context) .load(BASE_URL + exhibit.coverImg) .placeholder(R.drawable.placeholder) .into(holder.coverIv) holder.editBtn.setOnClickListener { onClick(exhibit) } holder.deleteBtn.setOnClickListener { confirmDelete(exhibit) } } override fun getItemCount() = exhibitList.size private fun confirmDelete(exhibit: Exhibit) { // 弹窗确认逻辑 } }图片上传这里用的方式是MultipartFile方式。Android端先通过系统相册或相机获取图片,然后转成文件流,通过OkHttp的上传请求提交给后端。需要提醒一个问题:Android 10及以上对文件读取有分区存储限制,如果直接从相册拿到的URI去读取文件路径,可能拿到的是异常路径(比如标题里热搜词中出现的/storage/emulated/0/android/data/...这类content://映射问题),此时需要通过ContentResolver读取,而不是直接拼接文件路径。写完这个逻辑后,务必在Android 11或12的真机上测试一遍,不然答辩现场拿模拟器演示可能看不出问题,一上真机就崩。
3.4 JWT Token 鉴权:前后端联调的关键一步
Token方案我在前面说了用简单Token,但如果你想让项目更有含金量,可以升级为JWT。JWT的结构包括Header、Payload、Signature三部分,在后端生成一个带有用户ID和角色的签名Token,前端存储在本地,每次请求带上,后端通过拦截器解析校验。
需要注意一个经典坑:JWT的Payload是Base64编码,不是加密的,里面的信息直接用工具就能解出来。所以千万不要把密码放到Payload里。我见过有同学图方便把用户密码放进去,答辩时被老师直接点出安全隐患,非常尴尬。Payload里只放userId和role,过期时间设24小时即可。
4. 远程调试、打包与部署的实战记录
4.1 小程序真机调试与预览
微信小程序开发工具虽然自带模拟器,但很多问题只有真机才会暴露。比如:模拟器上一切正常,真机上图片无法加载、request请求失败。
排查步骤一般是这样的:
- 确认
request接口地址不是localhost或127.0.0.1,真机不能访问你的电脑本机地址,必须用局域网IP或已备案的域名。 - 在微信公众平台后台配置合法域名。开发阶段可以在开发者工具中勾选“不校验合法域名”,但预览和发布时必须配置。
- 真机调试时,手机和电脑必须在同一局域网下,且关闭防火墙。
- 手机上看日志的方法:打开“vConsole”调试按钮,或者在真机调试模式下用开发者工具的Console面板查看。
另外补充一个很细节但很常见的问题:小程序请求的返回数据如果包含中文,在部分低版本安卓机的webview上可能显示乱码,这是因为响应头没指定charset。后端的Response加一行produces = "application/json;charset=UTF-8"即可解决。
4.2 Android远程调试的完整流程
提到远程调试,我知道很多人会搜到adb connect相关方法。这里讲一套适合毕设场景的实操方式。
有线调试:
- Android手机开启“开发者选项”,打开“USB调试”。
- USB数据线连接电脑,手机弹窗允许调试。
- Android Studio打开项目,点击Run按钮,选择连接的设备即可。
需要注意:部分国产手机(小米、华为、vivo等)的开发者在“设置-我的设备-全部参数”里连续点击“版本号”7次才能开启,不同厂商入口有差异。
无线调试(局域网):
- 手机和电脑连同一WiFi。
adb devices确认设备已连上。- 执行
adb tcpip 5555(需保持USB连接状态执行一次)。 - 拔掉USB,执行
adb connect 手机IP:5555。 adb devices看到设备列表出现手机IP:5555 device即成功。
有个高频排查点:连接失败时先检查手机端是否弹出“允许无线调试”的确认框,以及电脑防火墙是否拦截了5555端口。Windows系统还需要在“高级防火墙设置”里添加入站规则放行。
4.3 后端部署:从本机到服务器
毕设如果只在本机运行,演示时就要开着自己的电脑随时准备,风险较高。建议至少部署在一台云服务器上,便宜的也够用。
部署步骤大概是:
- 后端项目打包成jar包:
mvn clean package -DskipTests - 上传到服务器
/opt/app/目录 - 使用
nohup java -jar 项目名.jar > log.out 2>&1 &后台运行 - 配置MySQL数据库,导入建表SQL
- 通过
http://服务器IP:8080/api/...测试接口
服务器上放行端口时注意,MySQL 3306端口不需要对外开放(本地直连数据库即可),只要开放8080端口给前端调用就行。这样安全性更好,也少一个暴露面。
小程序上线需要HTTPS域名,如果只是本地毕设演示,用IP地址配合开发者工具“不校验合法域名”即可,不用买域名和证书。这笔钱能省就省,不是必须。
4.4 关于“远程调试服务”这件事
有些同学拿到源码之后,自己部署总是出问题。项目介绍里提到的远程调试、讲解,本质上就是帮你把环境跑起来、把问题定位清楚。以我个人的建议,凡是购买或获取毕设源码后,第一件事不是看功能,而是先把以下三样东西确定下来:JDK版本(8还是11或17)、MySQL版本(5.7还是8.0)、Node/小程序基础库版本。环境不匹配,后面全是兼容性问题,排查起来非常费时间。
如果你的项目也有“远程调试”环节,建议提前装好以下工具:adb(Android调试桥)、微信开发者工具、Navicat(数据库图形化工具)、Postman(接口测试工具)。这四样覆盖了App、小程序、后端、数据库四条线的调试需求,基本可以应对大部分求助场景。
5. 常见问题排查与答辩避坑要点
5.1 排查速度表:高频问题与对应方案
我把这个项目从搭建到演示期间最容易踩的问题整理成了表格,每一类都附上了排查顺序,方便卡住的时候快速定位:
| 问题表现 | 可能原因 | 排查顺序 |
|---|---|---|
| 小程序请求无返回 | IP错误 / 未配合法域名 / 后端未启动 | 先看后端日志,再ping通服务器,最后查域名配置 |
| 图片加载失败 | 图片路径存储了相对路径,但没有拼接服务器地址 | 检查数据库中img字段,是否完整URL |
| Android上传图片崩溃 | Android 10以上分区存储权限没适配 | 用ContentResolver读取,或申请存储权限 |
| 中文乱码 | 响应头缺少charset=UTF-8 | 后端统一设置编码 |
| 预约成功后列表看不到 | 用户ID没有传入查询条件,或时间格式比较错误 | 断点看SQL,直接打印接口参数 |
| adb连接设备离线 | USB端口冲突或驱动问题 | 更换USB线、重装驱动、重启adb服务 |
| 微信登录获取手机号失败 | 未申请接口权限 | 小程序后台申请,个人主体不支持则改用账号密码登录 |
5.2 一个至今印象深刻的Bug:预约时间跨天判断
这个Bug是我在这个项目中排查最久的一个。当时预约功能上线后,测试同学反馈“昨晚预约今天上午的时段,一直失败”。我看了半天代码,发现判断预约冲突时,只比较了日期和时段数字大小,没有把跨天情况考虑进去。
具体来说,appointment.date存的是yyyy-MM-dd,time_slot存的是09:00-11:00这种字符串。判断某用户在某个时段是否已预约时,使用的是字符串比较。正常同一天内没问题,但一旦日期从昨天跨到今天的午夜,字符串比较的规则就出错了。最后我把判断逻辑改成先解析Date对象,再判断时间戳,彻底解决。这个案例在答辩时可以当“亮点故事”讲——既能说明你处理了实际生产问题,也能体现你的设计边界意识。
5.3 答辩时老师必问的几个问题
答辩提问一般会集中在几个方向,我提前说下怎么答:
“为什么用小程序不用App?”回答思路:使用门槛低,覆盖面广,受众无需安装即可访问,适合临时性、非高频的访客场景;管理端用Android原生App,因为管理操作频率高,需要较强的原生能力和数据维护体验。
“如果预约人数很多,怎么处理并发?”这里不建议只回答“加锁”。可以说:当前场景是校园展厅,并发量有限,采用数据库行锁或乐观锁即可;如果扩展到大型场馆,可以用Redis预扣库存加消息队列异步处理。这个回答既承认了当前实现,又展示了扩展思路。
“你的数据库为什么这么设计?”重点回答:业务驱动设计。例如exhibit.images用分隔符存储是为了减少关联查询,在低并发和小数据量下性价比最高;预约表加状态字段是为了支持审核流程。别背范式,讲业务场景。
“这个项目还可以怎么扩展?”建议方向:增加二维码扫码导览(每个展品贴码,扫一扫跳转小程序详情页)、增加微信消息模板通知预约审核结果、增加VR全景展厅、管理端增加数据报表导出。这些扩展既好实现,又能体现思考深度。
5.4 关于源码、文档和二次定制的实话
市面上这套项目的源码版本非常多,但核心大同小异。如果你手头拿到的是基础版,建议优先补全三块内容:
第一,注释和文档。毕设论文里的核心代码部分,必须有清晰的注释和功能说明。很多源码的注释几乎为零,你得自己补。添加注释的过程也是熟悉代码的过程,避免答辩时老师随手点开一个类,你根本不知道它是干什么的。
第二,数据初始化脚本。全套源码往往自带SQL文件,但里面可能没有合理的示例数据。你需要在正式演示前准备至少20条展品记录、5条公告、若干预约记录,让系统看起来在真实运转。空表和假数据页面完全是两种说服力。
第三,定制需求沟通清楚。需要定制的功能,动手前必须把边界理清。比如“展厅公告”要增加置顶功能、“预约”要增加人数上限、“展品详情”要支持视频播放。这些不是随口一个“加上就行”,牵涉字段、接口、页面多个环节。定制前画一个简单的页面原型或文字说明确认,比改完再返工效率高得多。
6. 经验沉淀:几件比写代码更重要的事
这个项目从头到尾做下来,我觉得有几件事是代码之外的,但对完成度和答辩效果影响巨大。
第一个是真机测试。模拟器上一切都好,真机全崩的现象在移动端项目里太常见了。尤其是Android的存储权限、小程序的基础库版本兼容,不到真机上跑一遍根本发现不了。我习惯在开发过程中就频繁真机验证,而不是做完一起测,否则问题会堆叠到后期,一次要排查好几个变量。
第二个是接口文档。不要等到写论文时才回忆接口长什么样。用Apifox或Postman把每个接口的路径、入参、出参、示例保存好,写论文和理逻辑都方便。小程序端如果改了字段,Android端也要同步改,两边没有文档对照,全靠记忆是很容易出错的。
第三个是录屏演示。有条件的话,把整套流程(小程序端预约、Android端审核、后台数据变化)录成视频,放答辩PPT里。效果远胜于现场开模拟器一步步操作。现场演示一紧张容易卡壳,录像是最稳妥的方式。
第四个是提前准备一台备用机。Android开发时的设备兼容问题极其玄学,同一套代码,两台不同厂商的手机可能表现完全不同。准备一台备用机,确保答辩当天至少有一台手机是确定能跑通的。
这个项目如果做得深入,后续还可以扩展到其他展厅场景。比如把展品类型改成文物、把预约改成座位预约、增加多语言导览等,核心架构都不用怎么动。最后再分享一个小技巧:不管代码多简单,答辩PPT里一定要放一两张“踩坑截图”或“调试现场图”,让老师看到你真实解决问题的过程,这比展示一堆技术名词更有说服力。祝大家毕设顺利。