
简介这份PDF文档系统展示“基于Android的大学生校园兼职APP”的完整设计方案适合Android开发者、毕业设计学生及移动端后端初学者参考。文档围绕C/S架构分别讲解Android客户端与PHP服务器端客户端覆盖注册、登录、发布兼职、兼职列表、兼职详情、报名及个人中心模块重点涉及线程http通信、Handler消息处理、自定义适配器绑定ListView服务器端以单入口main.php配合不同接口文件通过action参数分发请求并使用SQL完成用户校验与兼职信息管理。资源包仅含1个PDF文件约1.22MB可直接阅读目前已有377人学习/下载。读者可借此梳理客户端与服务器端协同开发的完整思路对完成同类课程设计、毕业设计或理解APP前后端交互有直接参考价值。1. 一个 Android PHP 的校园兼职 APP值得拆开看这套校园兼职 APP 的设计资料看起来像是早期 Android 开发者的课程设计但它的价值并不过时。整条链路从 Android 客户端到 PHP 服务端全部打通涉及注册、登录、发布兼职、兼职列表、详情、报名和个人中心正好覆盖了一个真实业务类 APP 的完整骨架。对刚学完 Android 基础、想找第一个前后端联调项目的开发者来说它比单纯看教程更接近实际开发有 HTTP 线程处理、ListView 适配器、PHP 单入口文件、Session 恢复、SQL 操作这些硬知识点。下面按照客户端线程模型、列表展示、服务端接口设计、登录态改造这条线拆开讲每个部分都能直接照着做。2. Android 客户端HTTP 线程模型与登录注册模块设计2.1 为什么登录请求不能直接写在主线程Android 应用的主线程负责 UI 绘制和事件分发早期版本在主线程做网络请求会直接抛NetworkOnMainThreadException。即使系统允许一个慢接口也会导致界面卡死、系统弹出“应用无响应”。所以在这类 APP 里登录和注册必须放到子线程中执行。原设计里每个请求都新建一个继承Runnable的线程类比如LoginThread、SignUpThread。这种方式直观但并不优雅代码重复度高。不过它的核心思想到现在也没有变子线程做耗时操作主线程更新 UI线程之间通过Handler和Message通信。2.2 LoginThread 的写法与 Handler 回调下面这段是参考原设计重新整理的登录线程例子逻辑和论文描述一致把用户名、密码通过 POST 请求传到 PHP 接口拿到结果后用Handler发回主线程。// LoginThread.java public class LoginThread implements Runnable { private String email; private String password; private Handler handler; public LoginThread(String email, String password, Handler handler) { this.email email; this.password password; this.handler handler; } Override public void run() { // 这里使用 HttpURLConnection 提交 POST 请求 String result httpPost(http://yourserver/main.php?actionlogin, email, password); // 把结果封装成 Message通过 Handler 发送到主线程 Message msg new Message(); msg.obj result; handler.sendMessage(msg); } private String httpPost(String urlStr, String email, String password) { StringBuilder sb new StringBuilder(); try { URL url new URL(urlStr); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(POST); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); conn.setDoOutput(true); // 参数拼装content 格式为 keyvalue String body email URLEncoder.encode(email, UTF-8) password URLEncoder.encode(password, UTF-8); conn.getOutputStream().write(body.getBytes(UTF-8)); // 读取服务端返回的 JSON 字符串 BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), UTF-8)); String line; while ((line reader.readLine()) ! null) { sb.append(line); } reader.close(); conn.disconnect(); } catch (IOException e) { sb.append({\code\:1,\msg\:\network error\}); } return sb.toString(); } }这段代码里有几个地方需要说明连接超时写的是 5000 毫秒校园网环境下建议保持这个值太短容易误判太长会让用户等太久。POST 参数需要做URLEncoder.encode否则中文或特殊字符可能乱码。返回结果用 JSON 字符串统一封装服务端成功后可以返回session_id和type客户端再解析这个 JSON。conn.disconnect()放在读取之后是为了保证流关闭后再断开连接避免连接池异常。Handler 这一侧在主线程里创建 Handler 并重写handleMessageHandler handler new Handler(Looper.getMainLooper()) { Override public void handleMessage(NonNull Message msg) { String json (String) msg.obj; // 解析 JSON跳转到首页或提示登录失败 } };这里用Looper.getMainLooper()而不是直接new Handler()是显式绑定主线程。在 Android 10 及以上版本裸new Handler()会被 lint 报警告建议优先绑定 Looper。2.3 注册、发布兼职模块的代码复用原设计里注册模块新建了SignUpThread发布兼职又新建了一个线程类本质上都是“POST 参数到接口、返回 JSON”。这个模式重复三次以上就应该抽公共类比如封装一个HttpTask把接口地址、参数、回调接口传进去public class HttpTask implements Runnable { private String url; private MapString, String params; private Callback callback; Override public void run() { String result doPost(url, params); callback.onResult(result); } public interface Callback { void onResult(String json); } }调用时传不同的action和参数即可不需要每个功能写一个线程类。后面如果要改成 Volley、Retrofit只需要替换HttpTask内部的网络实现UI 层不用动。2.4 这个线程模型在今天怎么改如果现在重新实现建议直接用 OkHttp 或者 Retrofit配合协程或 RxJava。但底层原理和这个设计是相同的子线程执行请求、主线程处理回调。下面是把登录请求映射成协程的典型写法suspend fun login(email: String, password: String): ResultString { return withContext(Dispatchers.IO) { // 在这里执行 HttpURLConnection 或 OkHttp 请求 } }这样看原设计的线程模型并不过时只是代码组织方式需要升级。实际做项目时一定要注意 Activity 销毁后线程还在回调的问题可以使用静态 Handler、弱引用或者生命周期感知组件来解决。2.5 接口参数约定要先行客户端和服务端在开发前最好把接口文档写清楚。这类 APP 里接口参数一般遵守下面这种约定功能action 参数请求方式关键请求参数说明登录loginPOSTemail, password返回 session_id、type注册sign_upPOSTemail, password, typetype 区分个人/企业发布兼职publish_partPOSTsession_id, title, salary, detail需要登录态兼职列表part_time_listPOST/GET无返回全部兼职 JSON兼职详情part_time_detailsPOSTpart_time_id返回单条详情报名兼职join_part_timePOSTsession_id, part_time_id检测重复报名这个表就是前后端联调的基础。早期很多团队不写接口文档结果客户端等接口等一周服务端改一个参数名客户端排查半天。建议把这个表放到项目 README 里后续维护也方便。3. ListView 复用与 BaseAdapter兼职列表与报名状态切换3.1 为什么不用默认数组适配器服务端返回的兼职列表包含标题、薪资、公司、发布时间等多个字段。Android 自带的ArrayAdapter只能把每个列表项当作一个字符串展示根本不够用。要展示结构化的数据必须重写BaseAdapter。原设计里用自定义适配器绑定数据方法是对的。这个模块的核心点是getView()的复用机制如果处理不好列表滚动时会频繁卡顿。3.2 重写 getView 与 ViewHolder 模式下面的代码是兼职列表适配器的标准写法// PartTimeAdapter.java public class PartTimeAdapter extends BaseAdapter { private ListPartTimeItem data; private LayoutInflater inflater; public PartTimeAdapter(Context context, ListPartTimeItem data) { this.data data; this.inflater LayoutInflater.from(context); } Override public int getCount() { return data null ? 0 : data.size(); } Override public Object getItem(int position) { return data.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { // 第一次创建 item 时才加载布局 convertView inflater.inflate(R.layout.item_part_time, parent, false); holder new ViewHolder(); holder.tvTitle convertView.findViewById(R.id.tv_title); holder.tvSalary convertView.findViewById(R.id.tv_salary); holder.tvCompany convertView.findViewById(R.id.tv_company); convertView.setTag(holder); } else { // 复用的 View 直接取出 ViewHolder holder (ViewHolder) convertView.getTag(); } PartTimeItem item data.get(position); holder.tvTitle.setText(item.getTitle()); holder.tvSalary.setText(item.getSalary() 元/天); holder.tvCompany.setText(item.getCompany()); return convertView; } static class ViewHolder { TextView tvTitle; TextView tvSalary; TextView tvCompany; } }这段代码的关键点有两个convertView为 null 时才加载布局和创建 ViewHolder滚动时复用已存在的 View减少findViewById的调用次数。数据绑定放到复用分支之外否则会出现列表项内容错乱。很多初学者把setText写进了if (convertView null)里滚动后数据就串了。使用的时候直接把 ListView 设置适配器ListView listView findViewById(R.id.lv_part_time); PartTimeAdapter adapter new PartTimeAdapter(this, data); listView.setAdapter(adapter);注意 listView 对应的布局要写在 XML 中Item 布局建议单独写一个item_part_time.xml里面至少包含标题、薪资、公司三个 TextView。3.3 用控件可见性切换报名状态兼职详情页是同一个页面处理两种状态个人用户没报名时显示“报名”按钮报名后变成“已报名”企业用户显示“发布兼职信息”按钮。这里用到了 Android 控件可见性控制。方式很简单先渲染所有控件再根据条件设置Visibility// 从 Intent 或静态变量中拿到用户类型 int userType getIntent().getIntExtra(type, 1); // userType 1 表示个人用户2 表示企业用户 if (userType 2) { btnPublish.setVisibility(View.VISIBLE); btnJoin.setVisibility(View.GONE); } else { btnPublish.setVisibility(View.GONE); // 个人用户根据是否已报名决定报名按钮状态 if (hasApplied) { btnJoin.setVisibility(View.GONE); tvApplied.setVisibility(View.VISIBLE); } else { btnJoin.setVisibility(View.VISIBLE); tvApplied.setVisibility(View.GONE); } }这三个枚举值需要区分清楚属性作用是否占布局位置常用场景View.VISIBLE显示控件占默认状态View.INVISIBLE隐藏但占位置占需要保持布局稳定时View.GONE完全隐藏不占按钮、标题按条件显隐在这个业务里报名按钮和已报名文本应该用 GONE因为两个控件不应该同时占位置。如果只是暂时隐藏用 INVISIBLE 也行但会导致详情页下方出现空白区域。3.4 列表刷新和数据倒序要显示最新发布的兼职客户端在拿到列表接口结果后可以倒序显示。原设计是在服务端 SQL 里写ORDER BY part_time_id DESC这样客户端拿到就是倒序。如果服务端不管顺序客户端可以用Collections.reverse()在拿到 JSON 数组后反转一次。列表接口大概率会有下拉刷新最简单的做法是重新请求一次列表接口然后调用adapter.notifyDataSetChanged()。注意不能直接替换 adapter 内部数组而不通知刷新那样 UI 不会更新。如果用了 RecyclerView还需要考虑 DiffUtil但这里还是 ListView 项目就不过度展开了。4. PHP 单入口文件与 Session 恢复服务端接口怎么设计4.1 main.php 单入口实现服务端把所有接口集中到一个入口文件客户端请求时统一访问main.php用action参数区分接口。这样做的优点很明显接口路径少逻辑集中公共参数统一处理。?php // main.php $action isset($_REQUEST[action]) ? $_REQUEST[action] : ; switch ($action) { case login: require_once login.php; break; case sign_up: require_once sign_up.php; break; case publish_part: require_once publish_part.php; break; case part_time_list: require_once part_time_list.php; break; case part_time_details: require_once part_time_details.php; break; case join_part_time: require_once join_part_time.php; break; default: echo json_encode([code 404, msg action not found]); break; } ?这段代码的逻辑解释如下$_REQUEST同时接收 GET 和 POST 参数客户端传什么都能拿到。但实际开发中建议用$_POST或$_GET分开避免接口语义混乱。switch分发到不同接口文件每个文件只处理自己的逻辑避免一个文件几百行。最后一行echo输出 JSON 格式错误信息方便客户端定位问题。单入口后续扩展也方便比如加一个actionchange_password只需在switch中加一个分支不需要改动其他接口文件。4.2 登录接口与 Session 生成登录接口是服务端最核心的部分。原设计是匹配public_user表中的email和password字段成功后返回session_id和type。下面是一个简化实现?php // login.php session_start(); $conn mysqli_connect(localhost, root, , campus_job); $email $_POST[email]; $password $_POST[password]; // 实际项目中不能直接拼接 SQL这里仅为还原论文逻辑 $sql SELECT id, type FROM public_user WHERE email $email AND password $password; $result mysqli_query($conn, $sql); if (mysqli_num_rows($result) 0) { $row mysqli_fetch_assoc($result); $_SESSION[public_id] $row[id]; $_SESSION[type] $row[type]; $resp [ code 0, msg 登录成功, session_id session_id(), type $row[type] ]; } else { $resp [code 1, msg 用户名或密码错误]; } echo json_encode($resp); ?这里需要指出几个问题密码明文比较是教学项目简化写法真实项目至少要用password_hash()加密存储用password_verify()校验。这个设计成文时间较早可以理解但照抄到生产环境会有严重问题。SQL 直接拼接用户输入存在注入风险。用预处理语句或者 PDO 是更好的选择。session_start()要放在任何输出之前否则会报“headers already sent”。4.3 发布兼职与 Session 恢复发布兼职接口要识别当前登录的用户是谁。客户端登录成功后拿到session_id发布请求时把这个值传过来服务端用session_id()恢复会话。?php // publish_part.php session_id($_POST[session_id]); session_start(); $public_id $_SESSION[public_id]; $title $_POST[title]; $salary $_POST[salary]; $detail $_POST[detail]; $conn mysqli_connect(localhost, root, , campus_job); $sql INSERT INTO part_time (public_id, title, salary, detail) VALUES ($public_id, $title, $salary, $detail); if (mysqli_query($conn, $sql)) { echo json_encode([code 0, msg 发布成功]); } else { echo json_encode([code 1, msg 发布失败]); } ?关键点在于session_id($_POST[session_id])必须在session_start()之前调用。PHP 会根据传入的 session id 去加载对应的会话数据然后$_SESSION[public_id]才能取到值。如果客户端传的 session_id 不存在PHP 会新建一个空会话public_id取出来就是 null插入数据库时会写入空值。所以发布接口还应该加一层判断if (empty($public_id)) { echo json_encode([code 401, msg 未登录或会话过期]); exit; }4.4 兼职列表与详情接口列表接口返回数据库所有兼职信息并按part_time_id倒序$sql SELECT * FROM part_time ORDER BY part_time_id DESC; $result mysqli_query($conn, $sql); $list []; while ($row mysqli_fetch_assoc($result)) { $list[] $row; } echo json_encode([code 0, data $list]);详情接口需要接收part_time_id只返回这条兼职的信息。报名接口则同时需要part_time_id和session_id。这里有个隐含逻辑报名接口插入报名记录前最好先查一遍这个用户是否已经报过名否则会重复报名。原设计没有明确提到这个校验但实际开发不能省。4.5 接口接口之间的权限边界发布兼职必须限制为企业用户。个人用户拿到session_id后也能调publish_part如果服务端不校验$_SESSION[type]是否等于 2身份就形同虚设。建议在publish_part.php开头加上if ($_SESSION[type] ! 2) { echo json_encode([code 403, msg 只有企业用户能发布兼职]); exit; }这就是前后端双端校验的边界问题。前端隐藏按钮只是用户体验真正的权限控制必须落在服务端。5. 登录态静态保存的坑用 SharedPreferences 换掉5.1 静态变量保存用户信息为什么会出问题原设计在登录时把用户信息直接静态保存在 Android 程序中个人中心模块不再请求服务器而是读取静态变量。这个方案在应用进程存活时没有问题但一旦出现下面三种情况登录信息就会丢系统内存不足杀掉进程Application 和静态变量一起被回收。用户从最近的 App 列表划掉应用进程被杀。开发调试时重启 Activity静态变量在进程存活时还在但如果进程重启就成了 null。静态变量还容易造成内存泄漏。如果静态变量保存的是一个 Activity 引用Activity 销毁后无法被 GC 回收旋转屏幕几次就可能内存暴涨。5.2 改用 SharedPreferences 保存登录态正确的做法是把登录信息持久化到本地。最轻量的方式就是用 SharedPreferencespublic class SessionManager { private SharedPreferences sp; public SessionManager(Context context) { sp context.getSharedPreferences(user_session, Context.MODE_PRIVATE); } public void saveSession(String sessionId, int userType, String nickname) { sp.edit() .putString(session_id, sessionId) .putInt(user_type, userType) .putString(nickname, nickname) .apply(); } public String getSessionId() { return sp.getString(session_id, null); } public boolean isLogin() { return getSessionId() ! null; } }apply()是异步写入内存和磁盘不会阻塞 UIcommit()是同步写入会在主线程产生 IO不建议在 UI 线程调用。登录成功后调用saveSession个人中心启动时读取SessionManager这样即使进程被杀重新打开 App 也能恢复登录态。5.3 用 Token 替代服务端 Session如果做到这一步可以继续把 Session 机制升级为 Token 机制。Android 端不需要关心 PHP 的 session id只需在登录成功后拿到服务端生成的一串随机字符串每次请求把它放在 Header 或者参数里。服务端收到 Token 后再去查对应关系// 登录时生成 token $token bin2hex(random_bytes(32)); // 保存到数据库或缓存中 UPDATE public_user SET token $token WHERE id {$_SESSION[public_id]};客户端保存 Token 这样的方式更接近现代前后端分离架构不依赖 COOKIE也方便后续接入多端登录。验收时可以做一个最简单的持续化登录测试登录 App 后记录当前列表页状态长按最近任务将 App 清掉再重新打开。如果还能直接进入首页说明登录态持久化生效如果被踢回登录页说明静态变量方案没有改干净。这一步验证很重要因为很多开发环境里进程不一定会被杀真机测试和模拟器行为也不完全一样。本文还有配套的精品资源点击获取