
简介这是一份面向安卓初学者的物流管理系统大作业基于Android Studio开发支持用户注册与登录并涵盖物流信息管理、包裹查询等核心功能适合用于课程设计、毕业设计或个人练手。项目以Java为主要编程语言配合SQLite数据库完成用户认证、包裹数据存储和状态检索等模块覆盖了Android开发中界面布局、数据持久化、事件处理等关键技术。资源以zip压缩包提供共752个文件大小约23.97MB主要包含XML布局文件、Gradle构建脚本、Java与Kotlin源码、APK安装包以及由构建生成的中间文件结构清晰可直接导入Android Studio运行。目前已有20225人学习参考价值较高。通过本包可掌握安卓项目从零搭建到功能实现的完整流程理解注册登录的校验逻辑、列表查询的展示方式以及异常提示的交互设计附带APK还便于在真机或模拟器中直接体验。1. 从 Android Studio 大作业到物流管理系统别让“注册登录”带偏了主线很多同学拿到“物流管理系统”这个题目第一反应是先做登录页用两天时间把注册登录磨得锃亮结果答辩时被老师问“运单状态怎么流转”“轨迹查不出来怎么办”才发现下一步根本还没做。反直觉的结论是注册登录在这个项目里只是门槛真正的评分点是物流业务能不能闭环——新订单能不能建、状态能不能从“待揽收”一路改到“已签收”、按单号或收件人能不能查轨迹。这篇文章按 Android Studio 大作业最常见的“物流管理系统 注册登录”组合给你一套直接在模拟器和真机上可跑的方案用 SQLite 做持久化用 SharedPreferences 做会话RecyclerView 展示运单列表最后补充调试和答辩收尾细节。适合赶大作业的学生也适合刚做 Android 业务项目、想看数据层怎么写的人。2. 搭好 Android Studio 项目骨架先把数据层和依赖定下来项目开始前最该做的不是画界面而是把依赖和数据库结构定住否则后面每加一个功能都要回头改数据表比写业务代码还痛苦。我一般会先在 Android Studio 里新建一个 Empty Views Activity 项目语言选 Java。Kotlin 也很容易写但大作业评审看的是逻辑清晰度Java 的样板代码更直白对新手也更友好。下面的内容都按这套工程来讲用到的库只有三个常见的 AndroidX 组件不引入网络框架因为大作业阶段用本地数据就能把完整流程讲清楚。2.1 项目配置gradle 依赖、编译版本与国内镜像源新建项目后第一步是打开 app 模块的 build.gradle确认编译参数和依赖配置。这里给出一份可以直接抄的版本注意 namespace 和 applicationId 要换成你自己的包名android { namespace com.example.logistics compileSdk 35 defaultConfig { applicationId com.example.logistics minSdk 23 targetSdk 35 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled false } } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation androidx.appcompat:appcompat:1.7.0 implementation com.google.android.material:material:1.12.0 implementation androidx.recyclerview:recyclerview:1.3.2 }compileSdk 35 对应 Android 15 的编译行为目前 Android Studio 新建模板基本也在这个档位minSdk 选 23 是为了覆盖 2016 年之后的设备避免在低版本系统上处理运行时权限和分屏适配减轻大作业工作量。targetSdk 跟随 compileSdk 保持一致防止系统行为变更导致的兼容问题。这三个依赖分别负责基础 Activity、Material 控件和运单列表的 RecyclerView大作业范围内不用再加 Retrofit、Room 这类重框架。国内网络拉取 gradle 依赖经常卡住如果同步项目时一直转圈就在项目根目录的 settings.gradle 里把仓库顺序调整一下加上阿里云镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } google() mavenCentral() gradlePluginPortal() } }这里的原理是优先访问国内可直连的镜像地址找不到的依赖再回落给 google 和 mavenCentral。把镜像放前面最省事你在 gradle sync 时能看到依赖下载速度明显变快。最好也留意一下 AGP 版本Android Studio 新版本默认用 AGP 8.x这种环境下 Java 17 是必须的所以上面的 compileOptions 不能省略。2.2 数据库表结构用户、运单、轨迹分开落表物流管理系统的数据模型比一般记账 App 多一层“状态变化”所以数据库设计要分成三张表用户表存登录账号运单表存当前状态轨迹表存每一步操作记录。这样做的最大好处是状态变更时不需要改动原运单的历史记录查询轨迹直接拿轨迹表的时间线即可。CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, phone TEXT, role TEXT DEFAULT 普通用户, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE waybill ( id INTEGER PRIMARY KEY AUTOINCREMENT, waybill_no TEXT NOT NULL UNIQUE, sender_name TEXT, sender_phone TEXT, receiver_name TEXT, receiver_phone TEXT, receiver_address TEXT, cargo_name TEXT, status TEXT DEFAULT 待揽收, current_location TEXT, create_user_id INTEGER, create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE track ( id INTEGER PRIMARY KEY AUTOINCREMENT, waybill_no TEXT NOT NULL, log TEXT, track_time TEXT DEFAULT (datetime(now, localtime)) );username 和 waybill_no 都加了 UNIQUE 约束这比在 Java 里手动判断“是否存在”更可靠。UNIQUE 约束会在数据库层面阻止重复重复插入时 SQLite 会抛异常我们捕获异常后转换成业务提示避免并发或快速点击按钮造成的重复注册。注意我没有在 track 表建物理外键因为大作业里删除运单时希望轨迹能单独清理用逻辑关联反而灵活。2.3 SQLiteOpenHelper 封装与回调模式有了建表 SQL接下来把它们放进 SQLiteOpenHelper。新建 DbHelper 类onCreate 里执行建表onUpgrade 里先用 DROP TABLE 清掉再重建。大作业不需要做数据库迁移这种简单粗暴的方式能保证每次改表结构后 App 重启就能拿到新表代价是原来数据会丢但演示阶段完全可以接受。public class DbHelper extends SQLiteOpenHelper { public static final String DB_NAME logistics.db; public static final int DB_VERSION 1; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(TableContract.CREATE_USER); db.execSQL(TableContract.CREATE_WAYBILL); db.execSQL(TableContract.CREATE_TRACK); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS track); db.execSQL(DROP TABLE IF EXISTS waybill); db.execSQL(DROP TABLE IF EXISTS user); onCreate(db); } }数据库操作不能放在主线程否则 Android 会弹 StrictMode 警告甚至直接卡到 ANR。大作业里不需要引入 ViewModel 和 LiveData我习惯用“回调接口 线程池”的方式处理异步简单直接public interface DbCallbackT { void onSuccess(T result); void onError(Exception e); }持有 DbHelper 的仓库类里维护一个单线程执行器耗时操作丢进去执行完成后用 Handler 切回主线程。这个模式只用一个接口类即可后面注册、登录、运单查询全部复用比一上来就上 MVVM 更适合快速验收。3. 注册登录模块校验、加盐、会话状态一处不漏注册登录是这个物流管理系统的入口它的完整性直接决定老师愿不愿意往下看。很多大作业只做了“能跳转”就算登录成功实际还缺输入校验、密码存储、会话保持和退出这几个环节。我做这类项目时会把登录做成一个独立模块界面和业务分开这样可以避免在 MainActivity 里堆几百行代码。3.1 注册页的输入校验和重复用户名处理注册页面至少要校验用户名长度、密码长度、两次密码是否一致、手机号格式。这四步如果写在点击事件里逻辑会非常散所以我一般抽取一个静态校验方法返回 null 表示校验通过返回字符串则是错误提示public static String checkRegisterInput(String username, String password, String confirm, String phone) { if (username null || username.trim().length() 3) { return 用户名至少3位; } if (password null || password.length() 6) { return 密码至少6位; } if (!password.equals(confirm)) { return 两次密码不一致; } if (phone null || !phone.matches(^1[3-9]\\d{9}$)) { return 手机号格式不正确; } return null; }注册按钮点击时先调用这个校验方法出错就用 Toast 展示不触发任何数据库操作。校验通过后调用 RegisterRepository 的 register 方法里面先查一次 username 是否存在不存在才执行插入。这里要注意插入操作仍要包 try-catch配合数据库的 UNIQUE 约束兜底否则两个用户同时注册同一个用户名时可能出现崩溃。3.2 密码不能明文存用 SHA-256 加盐很多大作业为了省事直接把密码明文存进 SQLite 的 password 字段老师翻阅代码时印象会大打折扣。正确的做法是加盐哈希每个用户生成独立的随机盐再把“盐:哈希值”存进同一个字段。真实生产环境会用 BCrypt大作业里自己实现 SHA-256 加盐也完全够用。public static String hashPassword(String password) { try { byte[] salt new byte[8]; new SecureRandom().nextBytes(salt); MessageDigest md MessageDigest.getInstance(SHA-256); md.update(salt); byte[] hash md.digest(password.getBytes(UTF-8)); return toHex(salt) : toHex(hash); } catch (Exception e) { throw new RuntimeException(e); } }登录校验时先从数据库取出包含盐的字符串按冒号拆成盐和原哈希再用相同算法算出输入密码的哈希进行比较。用 SecureRandom 而不是 Random 生成盐是防止随机数被预测使用 toHex 转成十六进制是为了把字节数组变成方便存储在 TEXT 字段里的字符串。注意登录对比时不要每次都重新生成盐否则永远匹配不上。3.3 登录查询与状态保持登录成功后需要把当前用户标记为“已登录”。Android 里常见的处理方式是用 SharedPreferences 保存一个 JSON 或几个 key-value。我一般会保存 user_id、username、role 三个字段因为后面的物流操作要记录创建人首页也可能根据角色展示不同按钮。public void saveLoginState(Context context, User user) { SharedPreferences sp context.getSharedPreferences(login_prefs, Context.MODE_PRIVATE); sp.edit() .putLong(user_id, user.getId()) .putString(username, user.getUsername()) .putString(role, user.getRole()) .apply(); }登录方法本身还是走仓库类这里贴一个关键片段public void login(String username, String password, DbCallbackUser callback) { executor.execute(() - { try { SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.rawQuery( SELECT * FROM user WHERE username ? LIMIT 1, new String[]{username}); if (cursor.moveToFirst()) { String stored cursor.getString(cursor.getColumnIndexOrThrow(password)); boolean matched PasswordUtils.verifyPassword(password, stored); if (matched) { User user new User(cursor); new Handler(Looper.getMainLooper()).post(() - callback.onSuccess(user)); } else { new Handler(Looper.getMainLooper()).post(() - callback.onError(new Exception(密码错误))); } } else { new Handler(Looper.getMainLooper()).post(() - callback.onError(new Exception(用户不存在))); } cursor.close(); } catch (Exception e) { new Handler(Looper.getMainLooper()).post(() - callback.onError(e)); } }); }这串代码里的 rawQuery 使用?占位符查询参数通过 selectionArgs 传入能有效防止用户输入 OR 11之类的注入。Cursor 用完要 close否则反复登录会让资源占用越来越高。登录成功后的回调里再调用 saveLoginStateUI 跳转到 MainActivity。3.4 自动登录、退出登录与权限拦截自动登录的逻辑很直接MainActivity 的 onCreate 中读取 SharedPreferences如果没有 user_id 就跳转 LoginActivity并 finish 当前页面。如果已经登录则直接把用户名显示到界面上。SharedPreferences sp getSharedPreferences(login_prefs, MODE_PRIVATE); if (!sp.contains(user_id)) { startActivity(new Intent(this, LoginActivity.class)); finish(); return; }退出登录时做两件事清除登录状态然后跳回登录页。很多同学只写了 remove 一个字段结果退出后再打开还是自动登录原因就是漏清了其他字段。最稳妥的做法是直接 clearsp.edit().clear().apply(); startActivity(new Intent(this, LoginActivity.class)); finish();如果项目里需要区分管理员和普通用户可以在这里做一层角色拦截。比如普通用户只能查看自己创建的运单管理员能看全部。这个逻辑放在 BaseActivity 的 onCreate 里统一判断能比每个页面写 if-else 干净很多。4. 物流核心业务运单状态机、轨迹记录与条件查询登录搞定后真正的物流业务才是大作业的得分区。这里的难点不是增删改查而是“状态流转 轨迹记录”怎么保持一致性。只看一个运单表状态改就改了但老师大多会追问“你怎么证明这个运单经过了哪些节点”所以要提前把轨迹表用好。4.1 运单状态机设计物流状态不能随意跳转比如已经签收的运单不能再变回“待揽收”。我习惯用一个状态机表格来约束代码里的允许流转同时在更新方法里做校验。这个项目用四个主状态加一个终止状态就够了当前状态允许流转到业务含义待揽收运输中、已取消已下单等待快递员取件运输中已派送、已取消快递在干线运输尚未到末端网点已派送已签收、已取消快递员正在派件已签收无流程结束已取消无流程终止状态字段在数据库里存字符串可读性比存 int 好写 SQL 排查时一眼能看到当前状态。为了简单这里不再引入单独的状态码表但代码中必须把状态校验写进更新方法。4.2 新建运单表单校验与单号生成新建运单时单号不能随便填否则用户输重后会被 UNIQUE 约束拦截。常见做法是生成一个带时间戳的单号比如返回当前时间加三位随机数确保同一秒内创建也不会冲突。public static String generateWaybillNo() { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmss, Locale.CHINA); String timePart sdf.format(new Date()); int suffix (int) (Math.random() * 900) 100; return SF timePart suffix; }提交按钮点击后先把收件人、发件人、物品信息填入 ContentValues调用 waybillDao.insertWaybill 返回新运单的 id。注意这里的插入要和轨迹表初始记录放在同一个事务里运单创建成功就必须插入一条“已下单”的轨迹否则出现运单详情页里空空如也的情况。public void createWaybill(Waybill waybill, long createUserId, DbCallbackBoolean callback) { executor.execute(() - { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { ContentValues values new ContentValues(); values.put(waybill_no, waybill.getWaybillNo()); values.put(sender_name, waybill.getSenderName()); values.put(receiver_name, waybill.getReceiverName()); values.put(receiver_phone, waybill.getReceiverPhone()); values.put(receiver_address, waybill.getReceiverAddress()); values.put(cargo_name, waybill.getCargoName()); values.put(status, 待揽收); values.put(current_location, 网点); values.put(create_user_id, createUserId); db.insert(waybill, null, values); ContentValues track new ContentValues(); track.put(waybill_no, waybill.getWaybillNo()); track.put(log, 订单创建等待揽收); db.insert(track, null, track); db.setTransactionSuccessful(); new Handler(Looper.getMainLooper()).post(() - callback.onSuccess(true)); } catch (Exception e) { new Handler(Looper.getMainLooper()).post(() - callback.onError(e)); } finally { db.endTransaction(); } }); }beginTransaction 和 setTransactionSuccessful 之间的所有写操作会一起提交中途发生异常时endTransaction 会自动回滚不会出现运单有表记录但轨迹丢了这种脏数据。这也是 5 年工作经验的老开发看到大作业代码时会重点认可的一个细节。4.3 状态流转同步更新运单与轨迹状态修改最容易犯的错是只 update waybill 表不写轨迹或者分开写导致中途崩溃。正确的做法还是事务将运单状态、当前位置、轨迹日志更新放进同一个数据库事务。public void updateStatus(String waybillNo, String newStatus, String location, String log, DbCallbackBoolean callback) { executor.execute(() - { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { if (!StatusMachine.canTransfer( getCurrentStatus(db, waybillNo), newStatus)) { throw new IllegalArgumentException(不允许从该状态流转); } ContentValues waybillValues new ContentValues(); waybillValues.put(status, newStatus); waybillValues.put(current_location, location); db.update(waybill, waybillValues, waybill_no ?, new String[]{waybillNo}); ContentValues trackValues new ContentValues(); trackValues.put(waybill_no, waybillNo); trackValues.put(log, log); db.insert(track, null, trackValues); db.setTransactionSuccessful(); new Handler(Looper.getMainLooper()).post(() - callback.onSuccess(true)); } catch (Exception e) { new Handler(Looper.getMainLooper()).post(() - callback.onError(e)); } finally { db.endTransaction(); } }); }这里的 StatusMachine.canTransfer 是一个静态方法内部读取 4.1 的状态机表做判断。throw new IllegalArgumentException 会中断事务之后 catch 捕获并回调错误信息UI 层可以用 Toast 展示“非法流转”。location 参数写入 current_location这样列表页能直接看到“包裹当前在哪”不用点进详情才知道。4.4 运单列表、详情与按单号搜索运单列表用 RecyclerView 展示适配器里绑定单号、收件人、状态和当前位置四个字段即可。查询方法要支持按单号模糊搜索和按状态过滤SQL 中用 LIKE 拼接public ListWaybill listWaybills(String keyword, String status) { ListWaybill result new ArrayList(); SQLiteDatabase db dbHelper.getReadableDatabase(); StringBuilder sql new StringBuilder(SELECT * FROM waybill WHERE 11); ListString args new ArrayList(); if (keyword ! null !keyword.isEmpty()) { sql.append( AND waybill_no LIKE ?); args.add(% keyword.trim() %); } if (status ! null !status.isEmpty()) { sql.append( AND status ?); args.add(status); } sql.append( ORDER BY create_time DESC); Cursor cursor db.rawQuery(sql.toString(), args.toArray(new String[0])); while (cursor.moveToNext()) { result.add(Waybill.fromCursor(cursor)); } cursor.close(); return result; }这里用WHERE 11只是方便动态拼接 AND 条件不会影响查询性能。LIKE 的搜索参数仍然通过占位符传入用户输入的%会先被当作普通文本处理不会被当成通配符注入。查询结果按 create_time 倒序排列最新创建的运单排最上面这也是物流 App 的主流交互习惯。详情页再根据 waybill_no 去 track 表按时间正序查出全部轨迹用 LinearLayout 循环 addView 或另一个 RecyclerView 展示即可。5. 收尾技巧真机调试、构建坑与演示数据项目功能写完后最让人头疼的往往不是业务逻辑而是 Android Studio 环境问题和答辩演示时的紧张。最后这里给你三个可操作的收尾项能省下不少现场翻车时间。真机调试建议直接用数据线连 Android 手机。先在手机的“开发者选项”里打开 USB 调试不同品牌对连接策略不一样小米手机还会要求打开“USB 安装”权限。连接后跑adb devices能看到设备序列号Android Studio 的 Run 配置里选这台设备即可。如果一直识别不到重点检查驱动和 USB 模式别选“仅充电”。用模拟器开发时记得把模拟器的数据存储位置放到非 C 盘否则大项目编译几次后 C 盘容易爆红。构建时如果遇到“tag number over 30 is not supported”这类 DEX 报错先别急着搜中文。这类报错大多跟依赖库的字节码版本或方法数超限有关优先检查 AGP 版本是否过旧、依赖里是否有重复的 support 包。也可以打开项目的gradle.properties加上android.enableDexingArtifactTransformtrue和 multidex 相关配置但大作业里最省事的办法还是删掉用不到的依赖保持项目精简。答辩前在 DbHelper 的 onCreate 末尾预置几条运单数据省得现场新建还要填半天表单。预置数据的代码可以写在 onUpgrade 之后插入后别忘了同步插入对应的 track 记录。这样演示时直接打开列表页点一条运单进入详情就能看到完整轨迹。现场如果要展示状态流转只改一条状态并刷新老师看到轨迹时间线变化比你重复演示十次新建订单更有效。本文还有配套的精品资源点击获取