简介:这是一套面向Android开发学习者与进阶者的12306火车票订票系统完整项目源码,适合希望以真实业务场景提升移动端开发能力的人群。项目覆盖登录注册、车次查询、订单管理、个人信息设置等模块,并串联UI设计、网络通信、JSON解析、数据存储、多线程、缓存、权限管理、通知服务、事件总线及测试调试等核心知识点,可作为课程设计或毕业设计的参考方案。资源包共201个文件,以80个class、29个java源码、35个xml布局、43个png图片为主,另含少量jar、db、properties等配置与数据文件,整体约1.1MB,结构紧凑便于按模块研读。目前已有1706人学习下载,读者可从中获取完整工程结构、界面布局思路、网络请求与数据持久化实现方式,以及多线程与组件通信的落地写法,适合对照源码理解Android应用从界面到后台服务的完整链路。
1. 拆开一个能跑通购票全流程的 Android 客户端:这套源码到底值不值得下
如果你正在找一个能完整跑通「查票—选座—下单—支付—订单管理」闭环的 Android 项目来练手或做二次开发,这套 12306 火车票订票系统源码大概率能省掉你从零搭架子的一两周。它不是那种只画了几个静态页面的 UI Demo,而是把车次查询、余票展示、乘客管理、订单状态流转这些真实业务链路都串起来了。适合两类人:一是刚学完 Android 基础、想找一个有网络请求、有本地缓存、有列表分页的中型项目来啃的开发者;二是需要快速搭一个票务类 App 原型、不想在基础架构上重复造轮子的团队。我拿到包之后第一件事不是看代码,而是先跑起来走一遍主流程,确认它到底「功能齐全」到什么程度——这一步很关键,后面会细说怎么验证。
2. 先看清架构再动手:模块划分与数据流向
2.1 从目录结构判断这套源码的成熟度
拿到一个 Android 源码包,我习惯先看根目录的build.gradle和settings.gradle,这两个文件基本能告诉你项目的 Gradle 版本、依赖管理方式以及是不是多模块。这套项目是单模块结构,app目录下按activity、adapter、bean、network、db、utils分包,属于典型的 MVC 向 MVP 过渡的写法。别小看这个判断,如果分包混乱、所有类都堆在根包下,后续你改任何一个功能都会像在垃圾堆里翻东西。
# 查看项目结构,重点看 app/src/main/java 下的包组织 tree -L 3 app/src/main/java/com/example/trainapp # 输出大致如下 # com/example/trainapp # ├── activity/ # 页面入口,含查询、下单、订单列表 # ├── adapter/ # RecyclerView 适配器,车次列表和乘客列表 # ├── bean/ # 数据模型,Ticket、Order、Passenger # ├── db/ # SQLite 帮助类,缓存乘客和订单 # ├── network/ # 网络请求封装,OkHttp + 回调 # └── utils/ # 日期格式化、Toast、SharedPreferences这个结构说明它把「数据」和「界面」做了基本分离,bean和network独立成包,意味着你替换网络层或改数据源时不用动 Activity 里的逻辑。常见做法是先把network包里的请求地址和参数格式摸清楚,因为票务系统的核心就是接口契约。
2.2 核心数据模型与本地缓存策略
票务类 App 绕不开两个东西:车次信息和乘客信息。车次是高频查询、低频变更,适合做短时缓存;乘客信息是低频变更、高频读取,适合落 SQLite。这套源码里db包下有一个DBHelper,建了两张表:passenger和order_cache。乘客表存姓名、证件号、乘客类型;订单缓存表存订单号、车次、座位、状态。
// DBHelper.java 关键建表语句 public static final String CREATE_PASSENGER = "create table passenger (" + "id integer primary key autoincrement, " + "name text, " + "id_card text, " + "passenger_type integer)"; // 0成人 1儿童 2学生 public static final String CREATE_ORDER = "create table order_cache (" + "order_id text primary key, " + "train_no text, " + "seat_type text, " + "status integer)"; // 0待支付 1已支付 2已取消参数说明:passenger_type用整型枚举而不是字符串,查询和判断都更快,也避免中文编码问题。order_cache用order_id做主键,天然去重,下单成功后插入或替换都方便。这里有个设计取舍——它没有用 Room,而是手写 SQLite,好处是依赖少、编译快,坏处是没有编译期校验,写错字段名要运行时才发现。如果你打算长期维护,建议换成 Room,但如果是练手或快速原型,手写反而更直观。
2.3 网络层封装与接口调用方式
network包里有一个HttpUtil类,基于 OkHttp 做了简单封装,对外暴露get和post两个静态方法,回调直接返回字符串。这种写法在早期项目中很常见,优点是调用简单,缺点是回调在主线程、没有统一错误处理。
// 查询车次的典型调用 HttpUtil.post("https://api.example.com/train/query", params, new Callback() { @Override public void onResponse(String response) { // 手动解析 JSON,更新 UI List<Ticket> tickets = parseTickets(response); adapter.setData(tickets); } @Override public void onFailure(Exception e) { Toast.makeText(ctx, "查询失败", Toast.LENGTH_SHORT).show(); } });逻辑说明:params是一个Map<String, String>,里面放出发站、到达站、日期。回调里直接更新 UI,说明它默认在主线程回调,这在 OkHttp 的异步请求里是正常的。但要注意,如果解析 JSON 耗时较长,会卡顿,常见做法是把解析放到子线程或用 Gson 直接映射。参数方面,日期格式必须是yyyy-MM-dd,出发站和到达站用中文全称,这些在utils里的DateUtil和StationUtil有对应转换方法,改接口时别漏了。
3. 跑通主流程:从查询到下单的完整操作链
3.1 环境配置与首次编译的注意事项
这套源码的compileSdkVersion是 30,minSdkVersion是 21,Gradle 插件版本 4.1.0。如果你用 Android Studio 最新版打开,大概率会提示升级 AGP,我的建议是先别升,按原版本跑通再说。升级 AGP 可能连带要改namespace、compileSdk和一堆依赖,容易在第一步就卡住。
// app/build.gradle 关键配置 android { compileSdkVersion 30 defaultConfig { minSdkVersion 21 targetSdkVersion 30 versionCode 1 versionName "1.0" } } dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.google.code.gson:gson:2.8.6' implementation 'androidx.recyclerview:recyclerview:1.1.0' }注意:OkHttp 4.x 需要 Java 8 支持,在build.gradle的compileOptions里要加sourceCompatibility JavaVersion.VERSION_1_8,否则会报NoSuchMethodError。这是第一个高频翻车点,很多人卡在这里以为是代码问题,其实是编译配置没对齐。
3.2 车次查询与列表渲染的实操步骤
主界面是一个搜索框加日期选择器,点击查询后走QueryActivity,拿到数据后传给TrainListActivity渲染。整个链路是:QueryActivity收集参数 →HttpUtil.post请求 → 回调解析 →Intent传列表 →RecyclerView展示。
// QueryActivity 中触发查询 String from = etFrom.getText().toString().trim(); String to = etTo.getText().toString().trim(); String date = tvDate.getText().toString(); Map<String, String> params = new HashMap<>(); params.put("from", from); params.put("to", to); params.put("date", date); HttpUtil.post(API_QUERY, params, new Callback() { @Override public void onResponse(String response) { Intent intent = new Intent(QueryActivity.this, TrainListActivity.class); intent.putExtra("train_list", response); startActivity(intent); } // ... });参数说明:from和to必须是中文站名,如果用户输入拼音或简写,需要在StationUtil里做映射。date的格式在DateUtil里统一处理,别在 Activity 里手拼。这里有个细节——它把整个 JSON 字符串通过Intent传递,而不是传ArrayList,好处是避免序列化问题,坏处是数据量大时可能触发TransactionTooLargeException。常见做法是只传必要字段或改用本地缓存中转,但作为练手项目,这个量级没问题。
3.3 下单与订单状态流转的实现细节
选好车次后进入OrderActivity,选择乘客、座位类型,提交订单。订单提交成功后写入本地order_cache,状态为「待支付」。支付是一个模拟操作,点击后把状态改成「已支付」。整个状态机很简单,但它是理解票务系统业务的关键。
// 提交订单 Order order = new Order(); order.setOrderId(UUID.randomUUID().toString()); order.setTrainNo(trainNo); order.setSeatType(seatType); order.setStatus(0); // 待支付 // 写入本地缓存 dbHelper.insertOrder(order); // 模拟支付 btnPay.setOnClickListener(v -> { dbHelper.updateOrderStatus(order.getOrderId(), 1); // 已支付 Toast.makeText(this, "支付成功", Toast.LENGTH_SHORT).show(); });逻辑说明:UUID.randomUUID()生成订单号,保证唯一性。insertOrder内部用insertWithOnConflict的REPLACE策略,重复提交不会报错。状态更新用order_id做条件,改完刷新列表即可。这里要注意,真实场景下支付回调是异步的,需要轮询或推送,但作为源码学习,同步模拟足够让你理解状态流转。
4. 避坑与排查:这套源码最容易卡住的五个地方
4.1 编译报错Program type already present
现象:编译时提示某个类重复,通常是android.support和androidx混用。原因:项目里部分依赖还是旧的支持库,而 Android Studio 默认启用 Jetifier。解决:在gradle.properties里确认android.useAndroidX=true和android.enableJetifier=true,然后File → Invalidate Caches / Restart,再重新同步。
4.2 网络请求返回 403 或超时
现象:查询车次一直失败,日志显示 403 或SocketTimeoutException。原因:源码里的接口地址是示例域名,实际不可用,或者没有配置网络权限。解决:先在AndroidManifest.xml里确认有<uses-permission android:name="android.permission.INTERNET" />,然后把API_QUERY换成你自己能访问的接口,或者用本地 Mock 服务返回固定 JSON 先跑通 UI。
4.3 列表滑动卡顿、图片错位
现象:车次列表快速滑动时卡顿,或者座位图标显示错位。原因:Adapter里没有做 ViewHolder 复用,或者在onBindViewHolder里做了耗时操作。解决:检查TrainAdapter是否用了ViewHolder模式,图片加载建议引入 Glide 或 Coil,别在主线程解码 Bitmap。
4.4 订单重复插入或状态不更新
现象:多次点击提交,订单列表出现重复记录;或者支付后状态还是「待支付」。原因:insertOrder用了默认的insert而不是replace,或者updateOrderStatus的where条件写错。解决:把插入改成insertWithOnConflict(..., CONFLICT_REPLACE),更新时确认order_id传的是同一个值,可以在更新后重新查询一次数据库验证。
4.5 日期选择器在低版本设备上崩溃
现象:在 Android 5.0 或 6.0 设备上点击日期选择器直接闪退。原因:源码用了MaterialDatePicker或某个高版本 API,没有做版本判断。解决:换成DatePickerDialog,或者在调用前加if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)判断。这是血泪经验,低版本兼容性测试不能省。
5. 二次开发与验证:把源码变成你自己的项目
5.1 替换数据源:从示例接口到本地 Mock
如果你想先不依赖后端,把 UI 和逻辑跑通,最快的办法是加一个本地 Mock。在network包下新建MockUtil,返回固定 JSON,然后在HttpUtil里加一个开关。
public class MockUtil { public static String getTrainList() { return "{\"trains\":[{\"trainNo\":\"G101\",\"from\":\"北京南\",\"to\":\"上海虹桥\",\"departTime\":\"08:00\",\"arriveTime\":\"12:30\",\"seatTypes\":[{\"type\":\"二等座\",\"price\":553,\"remain\":12}]}]}"; } } // HttpUtil 中 public static void post(String url, Map<String, String> params, Callback callback) { if (USE_MOCK) { callback.onResponse(MockUtil.getTrainList()); return; } // 原有网络请求 }参数说明:USE_MOCK是一个静态布尔值,调试时设为true,上线前改回false。这样你可以在没有后端的情况下完整走一遍查询、下单、支付流程,验证 UI 和数据库逻辑是否正确。常见做法是把 Mock 数据放在assets目录下,用InputStream读取,比硬编码字符串更灵活。
5.2 用日志和数据库检查验证功能完整性
跑通主流程后,别急着改代码,先做一轮验证。我一般会看三个地方:Logcat 里有没有未捕获异常、数据库里订单状态是否和 UI 一致、SharedPreferences 里登录态是否正确清除。
# 查看数据库内容(需要 root 或 debug 包) adb shell "run-as com.example.trainapp sqlite3 /data/data/com.example.trainapp/databases/train.db 'select * from order_cache;'" # 查看 SharedPreferences adb shell "run-as com.example.trainapp cat /data/data/com.example.trainapp/shared_prefs/user.xml"逻辑说明:run-as只对 debug 包有效,release 包需要 root。查数据库能确认订单是否真的写进去了,查 SharedPreferences 能确认登录态和缓存有没有残留。这一步能帮你发现很多「看起来正常但实际没存」的玄学问题。
5.3 一个我每次都会走的检查习惯
从那以后我每次拿到一个 Android 源码包,都会强制走一遍「编译 → 安装 → 主流程 → 杀进程重开 → 查数据库」这五步。编译过了不代表能装,装上了不代表主流程通,主流程通了不代表状态持久化正确。这套 12306 源码在这五步里,最容易出问题的是第三步和第五步——查询接口不通和订单状态不落库。把这两步盯住,剩下的就是按需改 UI 和接真实接口了。希望帮到你。
本文还有配套的精品资源,点击获取