简介:基于Android+XAMPP+MySQL的家校互动平台项目包,面向计算机相关专业学生和移动开发爱好者,目标是解决在家校沟通场景中搭建客户端、服务端与数据库协同工作的完整流程问题。项目采用C/S架构,前端为Android应用,后端基于XAMPP集成环境运行,使用MySQL存储家长与学校的互动数据,可从零理解账号管理、消息推送、后台维护等模块。压缩包整体约151.66MB,涵盖项目全套源码与完整设计文档,源码经测试校正可百分百成功运行,便于读者直接导入开发环境进行编译与部署,同时可根据文档梳理数据表结构、接口调用及界面布局,适合作为毕业设计或课程设计的参考蓝本。已有293人学习,源码目录结构清晰,配有详细说明文字,对希望完整复现该平台并在此基础上扩展功能的开发者有较高参考价值。
1. 当Android、XAMPP和MySQL放在同一个标题里:这不是又一个“登录注册”Demo,而是一条能落地的CS架构路线
看到“基于Android+XAMPP+MySQL的家校互动平台”这个标题,很多人的第一反应是“老、简单、课程设计味太重”。但把技术栈拆开看,它其实是把CS架构、数据库设计、Android联网交互一次讲透的最小闭环:Android只负责界面和交互,XAMPP里的Apache和PHP承担业务接口,MySQL管持久化数据,家校互动则定义了“公告、留言、成绩、考勤”这类真实业务。这个组合能解决的问题很直接——用最小成本搭建一个可演示、可扩展、每层代码都能说清归属的完整应用。它适合正在做课程设计或毕业设计的学生,也适合刚接触Android联网开发、想在前后端边界上补课的开发者。
2. 先立架构再写代码:Android、XAMPP、MySQL在CS模式下的三层分工与一次完整请求链路
不少开发者的习惯是打开Android Studio就写界面,写到一半才发现不知道数据从哪来。家校互动平台这种带账号、带角色权限、带消息流转的系统,最忌讳的就是没有架构直接上手。CS架构在这里的价值不是“听起来正经”,而是强制你把客户端、服务端、数据库拆成三个能独立替换的层,每一层的职责一旦清晰,后续所有接口设计和排错都会变得有据可循。
2.1 CS架构的三层边界:Android、Apache+PHP、MySQL各管哪一块
先明确标题里的“CS架构”是什么。它指Client/Server结构,Android端是Client,XAMPP里跑起来的Apache+PHP+MySQL整体是Server。Server内部还可以再往下拆,但站在Android的视角,整个后端就是一个HTTP服务,客户端只认URL和JSON。
Android端是最轻的一层,它只做三件事:渲染界面、接收用户输入、把输入变成HTTP请求发出去。它不直接访问MySQL,也不该在代码里出现数据库账号密码。很多初学者第一次翻车就是在这里——在Java代码里拼一个“jdbc:mysql://...”的字符串,试图让手机直连数据库。这种做法在技术上不是完全不能通,但它等于把数据库用户名密码发到了每一台手机上,任何抓包工具都能看到,而且MySQL默认也不允许远程主机直连。常见做法是Android拿着一个URL去请求Apache,服务端返回什么JSON,客户端就展示什么,数据库对手机完全不可见。
服务端是XAMPP里跑起来的Apache+PHP。Apache负责接收HTTP请求,按URL路径把请求交给对应的PHP脚本;PHP脚本负责检查参数、连接MySQL、执行SQL、把结果整理成JSON返回。这一层是业务逻辑的归属地,角色权限判断、参数校验、数据组装都在这里完成。为什么用PHP而不是在Android里直接拼数据?因为服务端可以统一控制“谁能看什么”,而客户端代码一旦发出去就不受你控制了。
MySQL在最下面,只管数据的持久化存储。它不关心请求从哪来,也不关心Android界面长什么样,只接受SQL语句并返回数据集。家校本里的用户、班级、公告、留言,最终都要落到表里。三个层级分清楚之后,整个系统变成一条单向调用链:Android -> Apache+PHP -> MySQL,每一层都可以独立替换,换数据库不影响Android代码,换Android界面也不动后端逻辑。
2.2 为什么选XAMPP而不是单独装MySQL:环境一致性省下的时间会在排错时加倍还给你
XAMPP是一键打包的集成环境,把Apache、PHP、MySQL(实际是MariaDB)、phpMyAdmin装到了一起。标题里写的是MySQL,很多初学者第一次打开XAMPP会发现数据库叫“MariaDB”,以为是装错了。这里提前说清楚:MariaDB是MySQL的分支,兼容MySQL协议,SQL语法和连接方式基本一致,phpMyAdmin里操作方式也几乎相同,你可以把它当成MySQL用,不要在这个问题上纠结。
为什么这个方案里普遍选XAMPP而不是手动装Apache和MySQL?三个原因。第一,环境一致性,XAMPP的目录结构和配置文件位置是固定的,换一台电脑、跟着教程走一遍就能把服务端跑起来,这对需要演示和答辩的场景非常关键,不至于因为本机环境特殊而在现场翻车。第二,排错半径小,服务起不来、端口被占、配置文件改错,XAMPP的控制面板会把Apache和MySQL的状态直接显示出来,问题出在服务端还是客户端一眼就能区分。第三,phpMyAdmin提供了可视化建表和查数工具,调试接口时能直接在网页上看表内容,比在命令行里敲SQL直观得多。
但这个选择也有明确的边界。XAMPP默认配置面向开发环境,Apache的并发能力有限,MySQL的默认配置也没有针对高并发调优,所以它适合课程设计、校内实训、小规模快速原型,不适合直接拿去做公网生产服务。如果你将来要把这个家校互动平台真正部署给一所学校用,需要换成云服务器上的正式环境、上HTTPS、做并发和备份策略。但在“把方案跑通、把逻辑讲清”这个阶段,XAMPP就是性价比最高的选择。
2.3 数据怎么流动:一次“家长查公告”的完整请求链路
把架构落到具体场景里看,一次普通的“家长查询公告列表”操作,完整链路是这样的:
- 家长打开App的公告页,Android根据当前用户班级信息拼出请求URL,例如
http://192.168.1.100:8080/notice_list.php?class_id=3。 - Android通过OkHttp或HttpURLConnection发起GET请求,这个请求经过Wi-Fi路由到达电脑的Apache端口。
- Apache收到请求,根据URL后缀
notice_list.php找到对应PHP文件,交给PHP解释器执行。 - PHP脚本先加载数据库连接配置,校验参数
class_id是否合法,然后执行参数化SQL查询。 - MySQL执行查询,把命中的公告记录返回给PHP,PHP逐条读取并组装成JSON数组。
- PHP通过响应头把JSON返回给Apache,Apache再原样发送给Android。
- Android收到响应后判断状态码和业务码,解析JSON,把公告标题刷新到RecyclerView上。
这条链路里每一步都可能出问题。Android拼错URL,请求到不了Apache;Apache端口被占,服务根本起不来;PHP的SQL写错,返回不了数据;JSON字段名和Android解析不一致,页面就显示空白。所以后面做联调时,我习惯先把第5步和第6步用浏览器或curl单独验证,确认接口通了再去查Android端的问题,否则很容易在两层之间来回猜。这个排查顺序后面第6章还会再展开。
3. 数据层落地:MySQL表结构设计与PHP REST接口的最小可用实现
架构立住之后,动手顺序应该是先做数据层,再做接口层,最后写Android界面。很多团队把这个顺序反过来,结果就是界面写完了等接口,接口写完了发现表不对,表改了接口又得跟着改,来回返工。正确做法是先想清楚“系统里有哪些实体、实体之间什么关系”,把表建好,接口和界面就都有了依据。
3.1 四张核心表:用户、班级、公告、留言
家校互动平台听着功能很多,通知公告、班级留言、考勤反馈、成绩查询,但落到数据上,核心其实就四类实体:谁在使用系统、用户属于哪个班级、系统要发布什么通知、用户之间产生了什么消息。围绕这四类实体把表建好,剩下的功能都是在这些表上做组合查询。
以下是一套最小可用的表设计,角色用role字段区分,班级用class_id关联。target_class_id为0时表示公告面向全部班级,这是家校本里很常见的一种设计,能少建一张“公告与班级关系表”,对初期项目来说足够实用。
| 表名 | 职责 | 关键字段 |
|---|---|---|
| classes | 年级班级信息 | id, grade_name, class_name, head_teacher_id |
| users | 用户、角色、所属班级 | id, username, password_hash, role, real_name, class_id |
| notices | 教师或管理员发布的公告 | id, title, content, publisher_id, target_class_id, created_at |
| messages | 家长与教师之间的留言 | id, from_user_id, to_user_id, content, created_at, is_read |
users.role用枚举值区分admin、teacher、parent、student四种角色,比用数字更直观,phpMyAdmin里也能直接看到含义。classes.head_teacher_id存的是users表里某位教师的id,初期可以不设外键约束,靠应用层保证数据正确,降低建表失败的概率。以下给出users和notices两张表的建表SQL,作为初始化脚本的起点:
CREATE TABLE `classes` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `grade_name` VARCHAR(20) NOT NULL COMMENT '年级,如2024级', `class_name` VARCHAR(20) NOT NULL COMMENT '班级,如3班', `head_teacher_id` INT UNSIGNED NULL DEFAULT NULL COMMENT '班主任用户ID', PRIMARY KEY (`id`), UNIQUE KEY `uk_grade_class` (`grade_name`, `class_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `users` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password_hash` VARCHAR(255) NOT NULL COMMENT 'password_hash()生成', `role` ENUM('admin','teacher','parent','student') NOT NULL DEFAULT 'parent', `real_name` VARCHAR(50) NOT NULL, `class_id` INT UNSIGNED NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `notices` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(100) NOT NULL, `content` TEXT NOT NULL, `publisher_id` INT UNSIGNED NOT NULL, `target_class_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0表示全校', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_target_class` (`target_class_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表时有三个细节值得注意。一是字符集统一用utf8mb4而不是utf8,防止家长留言里出现特殊字符或表情时插入失败。二是created_at用DATETIME DEFAULT CURRENT_TIMESTAMP,让数据库自动写时间,避免PHP和Android各自传时间导致格式不统一。三是password_hash字段长度给到255,因为后面要用的password_hash()函数生成的字符串长度不固定,太短会直接报错。
messages表结构与此类似,字段为from_user_id、to_user_id、content、created_at、is_read,加一个idx_to_user_read索引即可支撑“查询我收到的未读留言”这类高频查询。初期不要再加更多冗余字段,等接口真正需要时再补,表结构太复杂只会让接口调试变慢。
3.2 第一个PHP接口:连接配置与JSON输出格式
表建好后,先写一个所有接口都会用到的数据库连接配置,把字符集、时区、JSON输出函数统一收口。这样后面每写一个接口,只需要require_once 'config.php',不用每张表重复拼连接代码。以下是一个最小可用的config.php:
<?php // 统一响应格式与数据库连接,所有接口文件都引入本文件 header('Content-Type: application/json; charset=utf-8'); mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $conn = new mysqli('127.0.0.1', 'root', '', 'school_platform', 3306); $conn->set_charset('utf8mb4'); date_default_timezone_set('Asia/Shanghai'); function json_out($code, $msg, $data = null) { echo json_encode([ 'code' => $code, 'msg' => $msg, 'data' => $data ], JSON_UNESCAPED_UNICODE); exit; } ?>这段代码做了几件事:第一行强制所有接口的响应都按JSON格式输出并用UTF-8编码,这是中文不乱码的关键之一;第二行开启mysqli的严格报错模式,SQL写错了会直接把错误抛出来,而不是返回空结果让Android端猜;第三行建立数据库连接,school_platform就是数据库名,请改成你自己建的库名。json_out()函数统一封装返回格式,所有接口返回体都是code、msg、data三个字段,Android端解析JSON时只需要处理这一套结构。
有了配置,再写公告列表接口就很快。这个接口接收一个可选的class_id参数,返回该班级可见的公告列表:
<?php require_once 'config.php'; // class_id 允许为空,空值或0表示查询全校公告 $classId = isset($_GET['class_id']) ? intval($_GET['class_id']) : 0; $sql = "SELECT id, title, publisher_id, created_at FROM notices WHERE target_class_id = 0 OR target_class_id = ? ORDER BY created_at DESC LIMIT 20"; $stmt = $conn->prepare($sql); $stmt->bind_param('i', $classId); $stmt->execute(); $result = $stmt->get_result(); $list = []; while ($row = $result->fetch_assoc()) { $list[] = $row; } json_out(0, 'ok', $list); ?>这里必须说明两个关键选择。第一,为什么用prepare预编译而不是把变量直接拼进SQL串?因为class_id来自客户端请求,是可被篡改的输入,直接拼接等于把SQL注入漏洞开给所有调用者。用bind_param('i', $classId)绑定参数,i表示整数类型,既省去了手工转义的麻烦,也堵住了注入路径。第二,为什么LIMIT 20?家校互动平台的公告会随时间增长,一次返回全部数据会让列表越来越慢,分页是迟早要做的事。初期写死LIMIT 20,Android端上拉加载时再传page和offset就是后面的扩展点。
把notice_list.php放到XAMPP的htdocs目录下,浏览器访问http://127.0.0.1:8080/notice_list.php?class_id=1,能看到JSON返回就说明服务端链路通了。如果没有返回数据,先检查school_platform库里是否导入了测试数据,再检查mysqli连接的用户名密码是否和config里一致,这两个小问题占了新环境调试的大多数时间。
3.3 登录接口与token校验:为什么不能用纯MD5,最小可用方案怎么做
登录接口是家校互动平台的入口,也是安全上最容易踩坑的地方。很多老教程里直接存MD5密码,甚至明文密码,理由是本地演示不需要那么安全。但家校本里有家长手机号、学生成绩、考勤记录,一旦数据库泄露或被同学抓包,后果就不只是课程设计扣分的问题了。最小可用且正确的做法是PHP原生的password_hash()和password_verify()。
注册或初始化用户时,用password_hash($password, PASSWORD_DEFAULT)生成密码哈希存入users.password_hash字段。登录时取出该字段,用password_verify()比对:
<?php require_once 'config.php'; $input = json_decode(file_get_contents('php://input'), true); $username = trim($input['username'] ?? ''); $password = $input['password'] ?? ''; $sql = "SELECT id, role, real_name, class_id, password_hash FROM users WHERE username = ?"; $stmt = $conn->prepare($sql); $stmt->bind_param('s', $username); $stmt->execute(); $user = $stmt->get_result()->fetch_assoc(); if ($user && password_verify($password, $user['password_hash'])) { // 登录成功,生成一次性 token 并返回给客户端 $token = bin2hex(random_bytes(16)); // 实际项目中把 token 写入 token 表或存到 session // 这里先返回 token,Android 端后续请求带上即可 json_out(0, 'ok', [ 'token' => $token, 'role' => $user['role'], 'real_name'=> $user['real_name'], 'class_id' => $user['class_id'] ]); } else { json_out(1, '用户名或密码错误'); } ?>password_verify()会自动识别哈希里的算法标识,不需要你关心具体是bcrypt还是argon2。bin2hex(random_bytes(16))生成一个32位的随机十六进制字符串作为token,Token本身是随机的,不携带用户信息,就算被截获也无法反推出密码。请把生成token后的持久化逻辑补上:常见做法是建一张user_tokens表,字段为user_id、token、expire_at,每次请求时校验token是否存在且未过期。对课程设计来说,把token存到$_SESSION里也能跑通,但Session在移动端使用体验差,还是建议用token表。
这里多提一句:如果你在别人的源码里看到“md5(md5($password))”这种写法,不要照着抄进自己的项目。MD5固定散列在撞库工具面前几乎是透明的,这不是“增加几层循环”能解决的事。安全最终目标是让攻击者即使拿到数据库也拿不到明文密码,password_hash就是PHP官方给出的那条路径,不需要你自己发明算法。
4. Android端对接:登录、token与公告列表的实现细节
服务端接口跑通之后,Android端的工作就变成了“发请求、收JSON、渲染界面”三件事。但这三步里有不少细节,网络线程、超时时间、JSON字段映射、明文HTTP限制,每一个单独看都不难,串在一起就能把一个新手卡住一整天。
4.1 网络层选型:OkHttp+Gson还是HttpURLConnection
Android发HTTP请求有两种常见路线,一种是系统自带的HttpURLConnection,一种是OkHttp加Gson的组合。做家校互动平台这种有登录态、有列表展示、后续还要上传图片的项目,我建议直接用OkHttp+Gson,而不是用HttpURLConnection自己拼解析逻辑。
HttpURLConnection的优势是不引入第三方依赖,但它的API偏底层,连接池、超时重试、JSON解析都要自己写,代码量不小且容易出边界问题。OkHttp把连接池、超时、拦截器都封装好了,Gson一行代码就能把JSON字符串映射成Java对象,这套组合是Android社区里最主流的做法,遇到问题也最容易搜到解决方案。在build.gradle的dependencies里加上依赖:
dependencies { // 版本号请按你创建项目时的最新稳定版为准 implementation 'com.squareup.okhttp3:okhttp:4.x' implementation 'com.google.code.gson:gson:2.x' }加完依赖记得Sync一下。OkHttp 4.x是Kotlin写的但对Java完全兼容,直接按Java方式调用即可。Gson版本不用刻意追新,稳定版就够用。如果你在Android 6以下的设备上测试,还需要在AndroidManifest.xml里声明INTERNET权限,Android 6及以上安装时自动授权,但清单里仍要声明这条权限:
<uses-permission android:name="android.permission.INTERNET" />漏掉这个权限时,运行时不会崩,但所有网络请求都会走onFailure回调,错误信息是“Permission denied”,排查时容易走弯路。
4.2 登录请求与token存储:主线程与回调必须分开
登录页是最典型的联网场景。用户输入用户名密码,点击登录,Android把数据POST到login.php,拿到token后存起来,跳转到首页。下面这个示例只保留核心逻辑,重点是展示OkHttp的调用方式和token保存方式:
private void doLogin(String username, String password) { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); RequestBody body = new FormBody.Builder() .add("username", username) .add("password", password) .build(); Request request = new Request.Builder() .url("http://192.168.1.100:8080/login.php") .post(body) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { // 网络异常,主线程弹Toast提示 } @Override public void onResponse(Call call, Response response) throws IOException { String json = response.body().string(); // 用Gson解析,拿到code和data // 如果code==0,把token存到SharedPreferences // 然后切换到主线程跳转首页 } }); }这里有两个必须记住的约束。第一,OkHttp的enqueue()本身就是异步的,回调方法运行在子线程,所以不能在回调里直接更新UI或弹Toast,必须通过runOnUiThread()或Handler切回主线程。Android对不能在主线程做网络请求这件事是强制检测的,直接在UI线程里调用execute()会抛NetworkOnMainThreadException。第二,超时时间的设置方式是Builder链式调用,三个超时分别针对连接建立、读取响应、写入请求。本机联调时10秒绰绰有余,如果手机连的是公网服务器或弱网环境,可以放宽到15到20秒,太短会误伤慢网络,太长会让用户一直盯着空白页。
Token保存用SharedPreferences最简单,登录成功后写入getSharedPreferences("app", MODE_PRIVATE),之后每次请求都在HTTP请求头里加Authorization: Bearer <token>,PHP端在受保护的接口里校验这个token。不要把token保存在数据库或文件里,SharedPreferences对当前项目的安全级别已经足够,等做到生产级别再上加密存储。
4.3 公告列表:Gson泛型解析与空态处理
公告列表页是APP里最常见的“列表加载”场景。服务端返回的JSON结构长这样:
{ "code": 0, "msg": "ok", "data": [ { "id": 1, "title": "期中考试安排通知", "publisher_id": 3, "created_at": "2025-05-12 09:30:00" } ] }Android端定义一个Notice类,字段名要和JSON里的key一一对应。Gson解析data数组时,因为泛型擦除问题,不能直接写new TypeToken<List<Notice>>(){}.getType()以外的写法。常见的错误是把data直接强转成List<Notice>,运行时报ClassCastException,原因就是没告诉Gson内部元素的具体类型:
Gson gson = new Gson(); JsonObject root = gson.fromJson(json, JsonObject.class); int code = root.get("code").getAsInt(); if (code == 0) { Type listType = new TypeToken<List<Notice>>(){}.getType(); List<Notice> notices = gson.fromJson(root.get("data"), listType); // 更新RecyclerView的Adapter数据 }字段名映射是另一个高频坑。PHP端返回的字段是created_at,Java类里如果定义成createdAt,Gson默认反射匹配时会因为名字对不上而赋null,程序不报错,页面就显示空。解决方式有两个:把Java字段名改成和JSON完全一致,比如就用created_at;或者在Java字段上加@SerializedName("created_at")注解。我建议直接改Java字段名,代码可读性更好,也少一处映射关系。下拉刷新时,清空列表数据再重新加载,注意notifyDataSetChanged()要等数据源更新完后调用,否则会出现刷新后旧数据残留的视觉问题。
空态处理是列表页最容易被忽视的部分。MySQL里一张空表,接口返回data为空数组,Android端解析后List为空,此时界面上如果只显示一个空白RecyclerView,用户会以为App坏了。在做项目演示前,至少要处理两种空态:一种是接口正常但没有任何数据,显示一个居中TextView提示“暂无公告”;另一种是网络失败,显示“加载失败,点击重试”。这两种状态用ListView或RecyclerView搭配View切换即可,代码量不大,但对答辩演示和实际体验的影响非常大。
5. 联调避坑实录:连不上、乱码、明文HTTP被拦,这五个问题占掉90%排错时间
这个标题之所以值钱,是因为它踩过的坑全在“联调”这两个字里。单个看Android、XAMPP、MySQL都没问题,一旦把它们串起来,环境差异、编码差异、端口冲突全冒出来了。下面五条是我做这类项目时反复踩过的,每一条都按现象、原因、解决三层说清楚,希望能帮你把排错时间从三天压缩到三小时。
5.1 连不上:模拟器能通,真机死活连不上
现象:Android模拟器里访问http://10.0.2.2:8080一切正常,换成自己的手机安装同一个APK,请求全部超时。
原因:10.0.2.2是Android模拟器专门用来访问宿主机(也就是你电脑)的保留别名,真机上根本不存在这个地址。真机要访问你电脑上的XAMPP服务,必须使用电脑在局域网里的真实IP,比如192.168.1.100,而且手机和电脑必须连着同一个Wi-Fi或同一个局域网。除此之外,电脑的防火墙默认会拦截外部设备对Apache端口的访问,真机请求到了防火墙这一层就会被丢掉。
解决:把接口地址统一收敛到一个常量里,比如在Java里定义一个BASE_URL字段,全部替换成http://192.168.1.100:8080。获取电脑IP在Windows上执行ipconfig,在macOS或Linux上执行ifconfig,找到IPv4地址填进去。防火墙处理上,Windows可以在控制面板的防火墙设置里允许Apache的专用网络访问,或者用管理员权限执行一条命令开放端口,比如开放8080端口供局域网访问。改完后用手机浏览器直接访问http://192.168.1.100:8080/notice_list.php,能出JSON再装App,能省掉大量反复安装调试的时间。
5.2 中文乱码:数据库正常,接口返回“???”
现象:phpMyAdmin里看数据表,中文显示完全正常;浏览器访问PHP接口,中文变成一堆问号或乱码;Android端接到的数据同样乱码。
原因:这是一条链路上的多数组装问题,任何一个环节编码不一致都会出乱码。常见的有三种:PHP文件本身被存成了非UTF-8编码;mysqli连接MySQL时没指定字符集;MySQL表或字段是latin1字符集。这三个问题只要有一个存在,中文就可能在“PHP -> MySQL -> JSON -> Android”的某一跳上坏掉。
解决:按顺序排查。第一步,PHP文件一律用UTF-8无BOM编码保存,特别是config.php,它定义了所有接口的响应头。第二步,MySQL连接后执行$conn->set_charset('utf8mb4'),确保读写都走UTF-8,这一步在config.php里已经写进去了,从自己代码里搜一下是否被删掉。第三步,建表时所有表都用utf8mb4,如果表已经建成了,用ALTER TABLE notices CONVERT TO CHARACTER SET utf8mb4;转换。最后,接口响应头加上header('Content-Type: application/json; charset=utf-8'),Android端用response.body().string()拿到的是原始UTF-8字符串,不要手动再转码。这四步做到位,中文乱码基本绝迹。
5.3 Android 9+明文HTTP被拦:请求没出错,但报CLEARTEXT
现象:App装到Android 9及以上的手机或模拟器上,所有http://开头的接口请求都失败,Logcat里能看到CLEARTEXT communication to 192.168.1.100 not permitted by network security policy。
原因:Android 9开始,系统默认禁止应用使用明文HTTP流量,只有https://才能直接访问。XAMPP默认跑的是HTTP,所以请求在系统安全策略这一层就被拦下了,不是代码逻辑的问题。
解决:开发调试阶段最直接的办法是在AndroidManifest.xml的application标签上加android:usesCleartextTraffic="true",表示允许整个应用使用明文流量。这一步做完重新编译安装,请求立刻恢复。但要注意,这个属性会让所有HTTP流量都放行,生产环境不应这样用。更规范的做法是配置networkSecurityConfig,只对指定的开发域名放行明文流量,Android官方文档有完整示例,这里不做展开。对于课程设计或本地演示,usesCleartextTraffic="true"是效率最高的选择,等部署到正式环境、换成HTTPS后再收紧。
5.4 时间差8小时:数据库时间对,App显示少8小时
现象:MySQL里created_at显示2025-05-12 09:30:00,浏览器里看接口也是这个值,但Android端显示2025-05-12 01:30:00,整整差了8小时。
原因:这个8小时差通常不在MySQL,而在Android端解析时间时用了系统时区。中国大陆是UTC+8,默认时区在北京时区时不会出错,很多人第一次遇到是因为手机或模拟器时区被设成了非中国时区,或者代码里用SimpleDateFormat没指定时区,直接按设备默认时区解析,时间就偏了。
解决:两个方向都能修。服务端方向已在config.php里写了date_default_timezone_set('Asia/Shanghai'),确保PHP输出的时间就是东八区时间。客户端方向,解析created_at时建议用带时区的格式化方式,在SimpleDateFormat里套上TimeZone.getTimeZone("GMT+08:00"),而不是依赖设备当前时区。另一个更稳妥的做法是让PHP接口直接返回时间戳,比如用strtotime($row['created_at'])把created_at转成整型时间戳,Android端解析成Date后自己决定格式化样式,这样数据库怎么存都不影响展示。初期项目推荐后一种,时间显示问题一旦做进协议层,后面所有接口都能收益。
5.5 端口被占:XAMPP启动面板Apache或MySQL红着
现象:打开XAMPP控制面板,Apache或MySQL的状态不是绿色,而是红色;偶尔Apache是绿的,但浏览器访问http://127.0.0.1却进了另一个服务的页面。
原因:80端口和3306端口是很多软件的默认占用目标。Apache默认监听80端口,如果有IIS、某些网盘客户端或其他Web服务已经把80占了,Apache就起不来。MySQL的3306同理,本机如果装了其他数据库服务,端口冲突会让MySQL启动失败。
解决:xampp控制面板上直接修改端口:Apache的httpd.conf里把Listen 80改成Listen 8080,同时把ServerName localhost:80改成localhost:8080;MySQL端口如果冲突,在my.ini里改port=3307。改完后所有接口URL里的端口都要对应调整。排查占用情况时,Windows上执行netstat -ano | findstr :80,能看到占用PID,再去任务管理器确认是哪个程序;macOS上执行lsof -i :80。这是典型的一次配置改三处的问题,改了Apache端口却忘了改Android端的BASE_URL,也会让联调继续失败,所以端口修改后一定要全局搜索一遍旧端口号。
6. 从能跑到能用:接口验证顺序与三个值得做的进阶动作
项目能跑和能演示之间,隔着一条验证链条。我见过太多人写完代码直接装App,页面白屏就开始改Android代码,最后发现是服务端接口压根没通。正确的验证顺序一定是:先验证接口,再接入客户端。用浏览器或curl直接访问http://127.0.0.1:8080/notice_list.php?class_id=1,看到合法JSON后再往下走。如果是POST接口,用curl模拟一次:
curl -X POST http://127.0.0.1:8080/login.php \ -H "Content-Type: application/json" \ -d '{"username":"parent01","password":"123456"}'返回里能拿到code、token、role,说明服务端正常,此时Android端再报错,问题一定在客户端解析或权限配置上。维护一份接口文档也相当值当,每写完一个接口就记录URL、请求参数、返回示例,这份文档既是给自己留的排错地图,也是这类项目交付时“文档”部分最实在的内容。接口定义表里写清字段名和类型,能避免Android端一个字段名对不上而排查半天。
三个进阶动作比较值得做。第一,上HTTPS,XAMPP自带Apache的SSL模块,配置一张自签证书后,Android端访问https://地址即可,这是从开发环境走向真实环境的必经一步。第二,把token改成带过期时间的方案,在user_tokens表里加expire_at字段,每次请求校验有效期,过期要求重新登录,这比“永久有效token”更接近生产标准。第三,把本地XAMPP迁到云服务器时,导出SQL、上传PHP文件、改数据库连接配置、在云控制台放行端口,这几步做完,同一个App就可以从手机远程访问,演示的说服力完全不一样。
我早期做这类项目总是先写界面再调接口,结果有一次在答辩现场Demo翻车,原因不是App崩溃,而是服务端压根没启动,页面一直转圈。后来我固定了习惯:先建表,再写接口,用curl把每个接口验一遍,最后才写Android页面;每次换机器或换网络,先用手机浏览器访问一遍接口地址,再装App。这套流程看着慢,实际是整个项目里最省时间的部分。希望这些踩坑记录能帮你在做家校互动平台这个方向时少走几段弯路,也祝你把这条CS架构路线真正跑通、讲透。
本文还有配套的精品资源,点击获取