news 2026/9/25 13:23:20

智慧校园双端系统开发:客户端与管理端的数据链路全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
智慧校园双端系统开发:客户端与管理端的数据链路全攻略

简介:一份基于Java实现的智慧校园Android客户端与管理系统源码项目,面向高校师生、Java/Android方向在校学生及毕业设计者。项目覆盖校园资讯浏览、点赞评论与分享,支持个人任务提醒、进度管理,以及团队任务安排、申请与资讯发布等常见智慧校园业务,适合用于毕设、课设或初期项目演示。压缩包共763个文件,包含218个Java源码、210个XML配置与界面、260张PNG图片资源,以及so库、jar依赖、Gradle构建脚本和说明文档等,整体大小26.8MB,目录结构清晰,便于按模块检索和二次开发。目前已有106人学习下载,源码经测试运行成功,功能稳定,答辩平均分96分。下载后可打开说明文档快速导入,运行中遇到问题可私聊远程教学,也可在现有基础上扩展功能,满足课程设计、毕业设计或项目立项演示需要。

1. 智慧校园这题目,难不在“智慧”,在“客户端和服务端怎么对齐”

很多同学拿到《基于Java实现智慧校园Android客户端及管理系统》这种高分毕设题目,第一反应是去网上找一个“课程表App”改一改。真动手才发现:课程表只是插在名单里的一个页面,整个项目真正重的是一套“双端系统”——Android客户端跑在学生手机上,Java管理系统跑在教务老师电脑上,两边靠HTTP接口和JSON数据对齐。智慧校园的范围很广,常见会拆成用户登录、课表查询、通知公告、选课、成绩、考勤几条线。新手容易卡在“客户端和服务端”怎么联动:数据库表建好了但接口没定,接口定好了但客户端没做加载态,演示时一断网就翻车。这篇文章按“后端管理系统→Android客户端→数据同步→踩坑记录→答辩验证”的顺序,把它拆成可以直接复现的落地步骤,适合做Java课程设计、毕业设计或想系统练一遍前后端链路的人。

2. 先立后端管理系统:数据库、接口、权限三个骨架怎么定

智慧校园的管理系统是“总装厂”:教师发公告、排课表、管理员加学生账号,都在管理端完成。Android客户端只负责把结果展示出来。管理端按Web后台来做,技术上还是Java——Spring Boot包一层Controller和Service,和Android客户端共用同一套数据库和业务逻辑。很多人先写客户端再补后端,结果接口改一次,客户端跟着改一次,来回折腾。我一般会先把管理端这三点定下来:数据库表关系、登录接口和统一返回格式、会话状态方案。这三件事决定客户端后面好不好写。

2.1 表设计先想清楚:学生、班级、课程、课表、通知之间的三张核心表

先看最常用的角色:学生、老师、管理员。一次登录要同时服务三种人,用户表不能和“学生信息”混在同一个表里。常见做法是:一张sys_user只存账号、密码、角色、姓名,再按角色拆学生表和老师表。比如学生表只存学号、班级、入学年份,老师表只存工号、学院、职称。下面这段DDL是跑通的最小集合,放在MySQL 8.x下能直接执行:

CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT NOT NULL DEFAULT 2 COMMENT '0管理员 1老师 2学生', real_name VARCHAR(50) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, class_name VARCHAR(50) NOT NULL, enroll_year VARCHAR(10) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(100) NOT NULL, teacher_user_id BIGINT NOT NULL COMMENT '关联 sys_user 里的老师', week_day TINYINT NULL COMMENT '星期几 1-7', start_section TINYINT NULL COMMENT '第几节开始', end_section TINYINT NULL COMMENT '第几节结束', classroom VARCHAR(50) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

课程表只存“课程开课信息”还不够,学生和课程是多对多关系,需要一张student_course关系表,记录哪个学生选了哪门课。否则一个学生查课表时,课程表上不知道放谁的课。关系表的最小字段是id、student_id、course_id,再加一个选课时间做时间戳。这四张表(sys_user、student、course、student_course)就能支撑登录、课表、角色区分这三个最基础功能。

为什么要这样拆?同一个用户名只能登录一次,但“登录后是谁”由sys_user.role决定。比如Android客户端打开登录页,输入账号密码,后端校验通过后,根据role返回不同菜单:学生看到“我的课表”,老师看到“我要代课”,管理员看到“用户管理”。这个设计也是Java面试题里常问的“用户权限模型”最小实现,学会了后面加菜单权限表也不迷路。

2.2 用 Spring Boot 把管理端接口立起来:登录接口和统一返回体

管理端接口的写法不止一种,最常见的是Spring Boot + MyBatis-Plus。先约定统一返回体,所有接口都返回同一个JSON结构,Android客户端解析时只需要关心code、msg、data三个字段。好处是接口多了以后,客户端网络层不需要为每个接口写单独的解析逻辑。下面是一个最简单的Result类:

public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMsg("ok"); r.setData(data); return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.setCode(500); r.setMsg(msg); return r; } // getter/setter 省略 }

逻辑说明:ok和fail是两个静态工厂方法,一个代表业务成功,一个代表业务失败。注意业务成功永远是code=200,HTTP状态码也保持200,这样客户端只看code就行。失败时code=500,HTTP状态码依然可以返回200,把业务错误交给客户端弹提示。这样做的好处是网络层和处理层分离,不会出现“接口404但客户端误以为是账号被禁用”的问题。

登录接口写起来也不复杂。Controller接收JSON用户名密码,Service里比对数据库,成功就发一个token。这里贴一个简化版:

@PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { // 1. 查用户 SysUser user = sysUserService.findByUsername(dto.getUsername()); if (user == null) { return Result.fail("用户不存在"); } // 2. 比对密码(实际项目中用 BCrypt 加密存储) String md5 = DigestUtils.md5DigestAsHex(dto.getPassword().getBytes()); if (!user.getPassword().equals(md5)) { return Result.fail("密码错误"); } if (user.getStatus() == 0) { return Result.fail("账号已被禁用"); } // 3. 返回 token 和用户基础信息 LoginVO vo = new LoginVO(); vo.setToken(UUID.randomUUID().toString().replace("-", "")); vo.setRealName(user.getRealName()); vo.setRole(user.getRole()); return Result.ok(vo); }

参数说明:第一,username在数据库里有唯一约束,登录查询直接走索引;第二,密码用MD5只是演示,真实项目一般用BCrypt,因为MD5有彩虹表风险,论文里写“密码使用BCrypt加密存储”是加分项;第三,role决定客户端菜单,status控制账号能否登录,这两项是后续所有权限判断的根基。如果你是课程设计,建议再补一个登出接口,把Redis里的token删掉。

注意MySQL的role字段是TINYINT,Java实体里对应Integer,前端判断角色时不建议直接用魔法数字0/1/2,建议定义一个RoleConst常量类。否则管理端写“2”代表学生,客户端也写“2”,过两周自己都忘了2是什么意思。

2.3 为什么要配 Redis 做会话:客户端频繁轮询时后端不背锅

登录校验通过后,客户端每次请求都要带着身份信息。最常见的做法有两个:一个是后端用Session,一个是自己发token存Redis。智慧校园这种场景,Android客户端的学生端会频繁拉课表和公告,一旦同时在线人数上来,Session内存会越堆越高。我一般会用Redis做token的存储层,Redis天然带过期时间,内存管理比传统Session清晰。

先做一个简单的TokenService:

@Service public class TokenService { private static final String TOKEN_PREFIX = "smartcampus:token:"; @Autowired private StringRedisTemplate redisTemplate; public void saveToken(String token, Long userId, long expireSeconds) { redisTemplate.opsForValue().set(TOKEN_PREFIX + token, String.valueOf(userId), expireSeconds, TimeUnit.SECONDS); } public Long getUserId(String token) { String value = redisTemplate.opsForValue().get(TOKEN_PREFIX + token); return value == null ? null : Long.parseLong(value); } public void deleteToken(String token) { redisTemplate.delete(TOKEN_PREFIX + token); } }

逻辑说明:这个类表面上只是set/get字符串,但它解决了两个关键问题:一是token和userId直接映射,客户端不好伪造;二是管理端重置密码时能主动删token,让指定会话失效。参数说明:token过期时间设成2小时(7200秒),针对“记住我”场景再单独生成一个长期token,客户端和短期token一起用。

实际排查时,如果发现“一会儿能登录一会儿不能”,先拿Redis客户端可视化工具看一眼key是否还存在、ttl还剩多少。很多毕设项目做不出来,不是代码问题,而是Redis服务没启动,或者连接配置写错了。Redis和MySQL的连接配置建议用单独的application-dev.yml,别把数据库密码硬编码在代码里。把这块梳理明白,Android和Java面试题里最常见的会话管理也就基本稳了。

3. 把 Android 客户端做成真客户端:Java 原生网络层与界面刷新的最小实现

管理端定了接口,Android端的工作就是“把接口当数据源”。很多人写Android客户端容易犯一个毛病:把业务逻辑写在Activity里,一个文件几百行,网络请求、数据解析、界面刷新全揉在一起。真正要复现的路径是:先封装网络层,再处理登录态,最后才是界面展示。这三层分开,后面加新页面就不怕。

3.1 网络层:OkHttp 封装、响应解析和错误提示的区分

Android原生开发里,OkHttp是绕不开的库。网络层要做的不是“发一个请求”,而是把“请求成功但业务失败”和“请求失败”分开。业务上密码错误、账号禁用都是code=500,但客户端不能当网络错误处理,否则学生看到的提示永远是“网络异常”,完全不知道密码错了。

下面是一个适合当通讯底层的最小工具类:

public class HttpUtil { private static final int CONNECT_TIMEOUT = 15; private static final int READ_TIMEOUT = 20; private static final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS) .build(); public static void post(String url, String jsonBody, String token, Callback callback) { RequestBody body = RequestBody.create( MediaType.parse("application/json; charset=utf-8"), jsonBody); Request request = new Request.Builder() .url(url) .header("Authorization", token == null ? "" : token) .post(body) .build(); client.newCall(request).enqueue(callback); } }

参数说明:connectTimeout 15秒解决“服务器不响应”的卡顿,readTimeout 20秒给慢接口留余量。超时值不宜太小,学生宿舍Wi-Fi不稳定,10秒内连不上直接超时,体验很糟糕。Authorization头是后端习惯用的字段名,你也可以叫access-token,但客户端和后端必须保持一致。

因为OkHttp的enqueue回调在子线程,回调里不能直接改TextView,你需要用runOnUiThread或者Handler切回主线程。这块是新手翻车重灾区:从回调里直接findViewById然后setText,一运行就崩。最小正确写法是先判断code,再切主线程刷新。

HttpUtil.post(loginUrl, jsonBody, null, new Callback() { @Override public void onFailure(Call call, IOException e) { runOnUiThread(() -> Toast.makeText(LoginActivity.this, "网络异常,请检查连接", Toast.LENGTH_SHORT).show()); } @Override public void onResponse(Call call, Response response) throws IOException { ResultDTO dto = new Gson().fromJson(response.body().string(), ResultDTO.class); runOnUiThread(() -> { if (dto.getCode() == 200) { startActivity(new Intent(LoginActivity.this, MainActivity.class)); finish(); } else { Toast.makeText(LoginActivity.this, dto.getMsg(), Toast.LENGTH_SHORT).show(); } }); } });

这里用Gson做JSON反序列化,ResultDTO的三个字段和后端Result类对应。注意Gson默认将int解析成int,后端code=200时返回的data字段可能是对象也可能是数组,所以ResultDTO里的data要用Object类型,具体业务类型再二次转换。这一步不写清楚,后面每个接口都得写一套解析,越到后面越头疼。

网络层还要留一条调试路径:在OkHttpClient.Builder里加一个HttpLoggingInterceptor,Logcat里能看到完整请求和响应。不要等到出bug才加日志,客户端和后端联调时打开日志,一眼就能看出是参数传错还是后端报错。

3.2 登录页与主界面的状态切换:从 SharedPreferences 到内存态

客户端登录成功后,不能只跳转页面,还要记住“我是谁”。Android常见的本地存储是SharedPreferences,存token、用户角色、姓名。下次启动时先读token,有token直接进主界面,没有则停在登录页。

SharedPreferences sp = getSharedPreferences("user_info", MODE_PRIVATE); String token = sp.getString("token", ""); if (token.isEmpty()) { startActivity(new Intent(this, LoginActivity.class)); } else { startActivity(new Intent(this, MainActivity.class)); } finish();

这段代码在SplashActivity的onCreate里执行。注意token存本地是方便,但别只存token不存role。智慧校园客户端登录后,学生、老师、管理员的界面入口不一样:学生看到课表、通知、选课;老师看到教学任务;管理员看到用户管理。判断入口用的就是SharedPreferences里的role字段。role选错,后面接口传参也会跟着错。

主界面一般用四个底部Tab:首页、课表、通知、我的。首页放今日课程和公告,课表页用RecyclerView展示周一到周日课程,通知页拉公告列表,我的页显示姓名、学号和退出按钮。用Fragment + ViewPager还是BottomNavigationView都行,关键是每个Fragment只在onResume里拉数据,避免切Tab时每次都走网络。这里可以加一个“仅首次创建时加载”的布尔标记,减少流量消耗。

退出登录也不能忘。点击“退出”后,先调后端的登出接口删Redis里的token,再清本地SharedPreferences,最后跳回登录页。如果只清本地不清后端,旧token还能用一段时间,学生会觉得“账号被偷了”。

3.3 WebView 还是原生:通知公告模块的取舍

智慧校园里最常见的公告内容是一段带HTML的富文本,比如学院发的通知,可能有加粗、配色、图片。Android端有两种做法:一种是用原生TextView解析Spanned,支持有限;另一种是在通知详情页套一个WebView,直接加载后端传来的HTML内容。我一般会选WebView,因为管理端用富文本编辑器发的公告,样式复杂,原生TextView很难完全还原。

WebView webView = findViewById(R.id.web_view); WebSettings settings = webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setDomStorageEnabled(true); webView.setWebViewClient(new WebViewClient()); webView.loadDataWithBaseURL( "http://你的后端地址/", contentHtml, "text/html", "UTF-8", null);

参数说明:第一,loadDataWithBaseURL的第一个参数是baseURL,传后端资源根路径,HTML里的静态图片就能用相对路径访问;第二,text/html表示内容类型,UTF-8是编码,最后null代表不设置虚拟URL。这里最容易踩坑的是用loadData而不是loadDataWithBaseURL,loadData遇到中文字符和图片相对路径会乱码或加载不出来。

WebView还有一个生命周期问题:离开详情页时最好调用webView.destroy(),否则多个WebView占内存。像智慧校园这种学生端,通知页会频繁打开关闭,不销毁会卡顿。做法是在Activity的onDestroy里先移除视图再销毁。你可以在WebView外层套一个ViewGroup,onDestroy时removeAllViews再destroy。这样通知模块才算真正收尾。

4. 管理系统和客户端的数据同步:分页、图片上传、消息推送怎么落地

智慧校园管理系统和Android客户端不是两个孤立的App,真正的项目复杂度来自“同一份数据两个端都在操作”。管理端在后台发通知、传课表图片、改学生信息,客户端看到的是操作结果。要实现这种“同步感”,至少要打通三个细节:分页参数、文件上传路径、消息到达机制。

4.1 列表页分页与下拉刷新:客户端和服务端必须约定 pageNum/pageSize/total

通知列表、选课列表、成绩列表都是典型的分页列表。Android端下拉刷新、上拉加载,对应的就是页号pageNum和每页条数pageSize。服务端不能一口气返回所有数据,否则学生端几百条公告全塞进来,手机内存直接爆。后端接口用MyBatis-Plus的分页插件时,返回结构一般是下面这样:

{ "code": 200, "msg": "ok", "data": { "total": 47, "list": [ {"id": 1, "title": "国庆节放假通知", "publishTime": "2025-09-20 10:00:00"}, {"id": 2, "title": "图书馆闭馆通知", "publishTime": "2025-09-19 16:30:00"} ] } }

注意分页参数不要叫page和limit,业界更常见的是pageNum和pageSize。客户端请求时传两个参数,服务端验证pageNum从1开始,pageSize最大50。如果pageSize传10000,后端要拒绝,防止有人把所有数据一次性拖走。这是一个典型的“接口安全”细节,写在论文里能说明你想过边界情况。

Android端用SwipeRefreshLayout + RecyclerView实现分页加载:

swipeRefreshLayout.setOnRefreshListener(() -> { pageNum = 1; loadNotice(false); }); recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { LinearLayoutManager lm = (LinearLayoutManager) recyclerView.getLayoutManager(); if (lm.findLastVisibleItemPosition() >= adapter.getItemCount() - 1) { loadNotice(true); // 加载更多 } } });

loadNotice里真正请求时,拿到total后要和当前列表大小对比:如果当前列表大小已经大于total,就不继续加载更多。很多项目在这块会翻车:上拉加载一直触发,但服务端已经没数据了,接口一直返回空列表,造成死循环。解决办法就是加一个“isLoading”标志,在请求开始前置true,结束后无论成功失败都置false,防止回调重入。

4.2 图片上传与访问路径:管理端传图,客户端显示

智慧校园里的图片主要包括课程封面、校园新闻配图、学生头像。管理端用的是Web页面,上传图片后要保存到一个静态资源目录,Android客户端通过URL访问。这里最容易出问题的是“上传路径”和“访问路径”不一致。

后端用一个简单的上传接口:

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newFileName = UUID.randomUUID().toString().replace("-", "") + ext; String saveDir = "/data/smart-campus/upload/"; File dir = new File(saveDir); if (!dir.exists()) dir.mkdirs(); file.transferTo(new File(saveDir + newFileName)); return Result.ok("http://服务器IP:8080/upload/" + newFileName); }

参数说明:第一,originalFilename是客户端原始文件名,但最终保存时不能直接用原始名字,否则同名文件会互相覆盖,必须用UUID或时间戳重命名;第二,ext一定要取后缀,防止用户上传一个叫“xxx.jpg.exe”的文件时判断出错;第三,实际生产里还要校验文件类型和大小,比如只允许jpg/png,最大5MB。

访问路径上,Spring Boot需要在application.yml里配置静态资源映射,不然/upload/**路径会404:

spring: resources: static-locations: file:/data/smart-campus/upload/

很多毕设项目把图片保存在项目源码的resources目录,打包成jar后图片路径就找不到了。我一般不建议那样做,把上传目录放到项目外,比如/data/smart-campus/upload,再通过static-locations映射。这样重新部署项目不会把用户上传的图片弄丢。Android端加载图片时直接使用Glide加载返回的完整URL即可,不需要在客户端再拼一次文件名。

4.3 轻量级消息推送累了:用轮询还是长连接

“管理端发一条通知,学生端马上收到”是智慧校园最打眼的演示功能,但实现方案要选对。大多数毕设项目不会接入第三方推送SDK,因为没有服务器和证书环境。常规做法有两个:短轮询和WebSocket。

短轮询实现简单:学生端进通知页时拉一次,之后每隔30秒再拉一次。用Handler的postDelayed就能写,缺点是每次轮询都走一次网络,服务器压力大。为了演示,轮询时间设15-30秒最佳,太长演示时“老师等了半天没反应”,太短后台被轮询压垮。可以写一个正式一点的做法,用WorkManager的PeriodicWorkRequest,系统在后台自动跑:

PeriodicWorkRequest request = new PeriodicWorkRequest.Builder( NoticeSyncWorker.class, 15, TimeUnit.MINUTES) .build(); WorkManager.getInstance(context).enqueueUniquePeriodicWork( "notice_sync", ExistingPeriodicWorkPolicy.KEEP, request);

逻辑说明:WorkManager会合并任务、减少耗电,适合后台更新。但如果你要演示“发布公告后客户端30秒内看到”,就得把间隔改成30秒,或者用WebSocket做长连接。WebSocket的优点是实时性好,缺点是要维护连接状态和服务端心跳,教学项目里敲完也要花不少时间调试。我的建议是:毕设里用短轮询足够,在论文“不足与展望”里写一句“生产环境可替换为WebSocket”就能加分。

同步还要注意“删除”和“修改”的通知。如果管理端把公告撤回了,客户端本地已经缓存这条公告,重新拉列表后应该把本地旧的移除。最简单的做法是每次同步全量替换当前列表的前2页,不做增量合并;这样逻辑简单不容易出bug。增量合并要对比id,费时间还容易漏。

5. 智慧校园项目避坑:跑通和答辩前必须知道的问题

这里不写“预期怎么做”的空谈,直接列我自己踩过、也看学生反复踩的血泪经验。每一条按“现象→原因→解决”的结构来,方便你在答辩前对着检查一遍。

5.1 Android 9 以上强制要求 HTTPS:明文流量被拦下

现象:在Android Studio模拟器里访问后端接口,Logcat报错“CLEARTEXT communication to 192.168.1.10 not permitted by network security policy”,接口返回null,但浏览器访问同一个接口是正常的。

原因:Android 9(API 28)开始默认禁止明文HTTP流量,而智慧校园开发阶段后端通常是http://localhost:8080或局域网IP,不是https。很多新手不知道这个限制,以为是网络层写错了。

解决:在AndroidManifest.xml的application节点加android:usesCleartextTraffic="true",这是开发阶段最快的处理方式。如果目标应用要发布,不建议全局放开,可以只针对后端IP配置networkSecurityConfig。注意这只影响Android端,后端不用改。答辩时记得说明:正式环境会用HTTPS,开发期为了本地调试放开明文流量。

5.2 WebView 显示管理端 HTML 公告:中文乱码、CSS 错位

现象:通知详情页用WebView.loadData加载从后端接口返回的HTML,结果是中文字符变成“锟斤拷”,段落间距和图片位置完全不对。

原因:loadData方法默认把内容按HTML的编码头解释,但没有在后端HTML内容里声明charset,也没告诉WebView用UTF-8解码。另一种情况是CSS和图片链接用的是相对路径,loadData的baseUrl为空,资源全部无法加载。

解决:改用loadDataWithBaseURL,并明确传UTF-8,baseUrl传后端资源根地址,例如“http://你的服务器地址/”。同时要求管理端保存通知时,HTML内容统一转成UTF-8再入库。如果在后端用Java拼HTML,注意对图片标签的src做绝对路径拼接,别只存相对路径。

5.3 后端 LocalDateTime 返回的 JSON 格式,客户端解析不出时间

现象:管理端通知实体里用LocalDateTime作为publishTime,客户端拿到的JSON里publishTime却是一个对象结构,类似“{"date":{"year":2025...}}”,用Gson解析成String失败。

原因:Java 8的LocalDateTime在Jackson默认序列化时,会序列化成JavaTime模块的特殊结构,不是“2025-09-20 10:00:00”这种直观字符串。Spring Boot如果不配置Jackson的日期格式,就会发生这种“前后端字段类型不对齐”的问题。

解决:在Spring Boot的application.yml里配置日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时后端返回的时间字段类型统一用String接收,或在实体字段上直接加注解@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。客户端解析时publishTime也直接用String类型,不要在客户端再包一层Date转换。这样最简单的做法能把大部分时间格式问题规避掉。

5.4 管理端改了学生密码,旧 token 还是能继续访问接口

现象:管理员在管理系统里重置某个学生的密码,但学生手机上没退出登录,Android客户端仍然用旧token拉课表、查成绩,功能一切正常,安全上等于没有改密码。

原因:后端用token做身份识别时,只判断token在Redis里是否存在,没有让用户密码和token产生关联。密码改了,token依然有效,这就是典型的“客户端和服务端状态不同步”。

解决:在sys_user表加一个password_version字段,每次修改密码时把它加1;登录时把版本号对应的Redis值存成“userId:版本号”。拦截器从Redis取出userId后,再查一次当前用户版本号,不相等就判定token失效。这个方案比“主动调deleteToken接口”更工程化,因为它不依赖客户端配合。

5.5 打包 APK 后连不上本机后端:模拟器能跑,真机就是不行

现象:用Android Studio模拟器调试时,客户端请求http://10.0.2.2:8080一切正常。打包成APK安装到真机后,同样的页面转圈加载,请求全部失败。

原因:10.0.2.2是Android模拟器里分配给宿主机的特殊地址,只存在于模拟器网络环境中。真机上的10.0.2.2指向局域网里的某个设备,或者根本不可达。代码里把baseUrl写死成10.0.2.2,真机自然连不上。

解决:把baseUrl抽成一个可配置常量,放在BuildConfig里,不同构建类型用不同的值。调试版用10.0.2.2,发布版用后端服务器的局域网IP或公网域名。同时后端Spring Boot的启动端口不要绑定127.0.0.1,要监听0.0.0.0,否则真机从局域网过来也会被拒。检查方法:在Android手机上用浏览器打开http://后端IP:8080/upload/xxx.jpg,能打开说明网络通,打不开说明端口或防火墙问题。

6. 让这个项目多拿两分:离线缓存、接口日志和答辩演示技巧

项目基本功能做完、坑也踩完,已经能跑了。但课程设计验收和答辩,最看重的不是“能跑”,而是“你怎么证明它稳定”。我带过的学生里,翻车最多的不是在功能演示,而是现场网络断了,Android客户端一片空白。所以最后一件事:给客户端加一层离线缓存,至少保证课表和公告在断网时还能显示上次的数据。

离线缓存最轻量的做法是用文件或SharedPreferences存JSON。比如课表接口返回后,把整个data数组的JSON字符串存到SharedPreferences;下次进入课表页时先读本地数据渲染,再去拉网络,网络成功则覆盖本地。关键代码只有一行:

getSharedPreferences("cache", MODE_PRIVATE) .edit() .putString("course_list", jsonString) .apply();

读取时先判断本地是否有值,有值直接用,再异步刷新。这套逻辑简单,却能让答辩现场显得很专业,你可以直接说“我对读多写少的课表数据做了本地缓存,弱网环境下客户端不会白屏”。

第二个值得做的点是接口日志。在OkHttp的拦截器里打印请求URL、耗时、响应code,Logcat里就能看到每次接口调用的状态。答辩演示时打开Logcat,老师看到你一边点按钮一边有日志输出,比嘴上说“接口没问题”更有说服力。不要等到出bug才加日志,项目交出去之前就应该把Log留着。

最后一个技巧是答辩时的验证脚本:先用管理端新增一条公告,再用客户端下拉刷新看到这条公告,这个“闭环演示”比单独展示任何一张页面都重要。你可以在纸条上写“先改密码→再开客户端→观察token失效”这种小剧本,把前面踩过的坑当成亮点讲。我的习惯是每个功能至少准备一条失败路径的演示,比如“断网时客户端提示网络异常而不是闪退”,老师反而觉得你考虑得全。

智慧校园这个题目看着容易,真正拉开差距的从来不是功能数量,而是双端数据链路是否经得起追问。把这些细节打磨完,这个项目就不是“毕业设计”而是“作品”了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 13:21:07

英特尔Day 0适配Qwen3新模型:TaoToken统一Key打通AI PC智能体配置链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 13:20:38

IDEA 里配置 Trae AI 插件:从 settings.json 骨架到 TaoToken 统一 Key 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 13:13:59

高并发下缓存穿透与击穿的防御实践:基于Redis的封装方案

做了这么多年后端&#xff0c;缓存穿透和缓存击穿这个问题我几乎在每个高并发项目里都要重新讲一遍。最近我把这两类问题的防御逻辑统一封装成了一个可复用的工具包&#xff0c;基于Redis实现&#xff0c;核心围绕布隆过滤器、分布式锁、本地缓存和空值缓存这套组合拳。这篇就是…

作者头像 李华
网站建设 2026/9/25 13:13:48

Atlas 300V 24G部署YOLO实战:从硬件到模型转换与调优全指南

做了好几个月的边缘端目标检测项目&#xff0c;我一直想把这套完整的部署经验记录下来。正好最近看到有人问“atlas 300v 24g 是运算加速卡吗”&#xff0c;又有不少人在搜“atlas部署yolo”&#xff0c;就干脆把这块卡的定位、硬件细节、模型转换流程和实际踩坑经验全部整理成…

作者头像 李华