简介:这是一套面向高校学生与Android开发初学者的完整课程设计项目,采用Kotlin语言开发,涵盖外卖应用的客户端、服务端与数据库三层架构,适合作为大三课程设计参考或移动开发入门实战案例。压缩包共270个文件,约150MB,包含52个kt源码、42个xml布局、23个java文件、35个so库及5个apk安装包,另附sql建表脚本、gradle构建配置、docx设计报告与gif演示图,覆盖界面设计、业务逻辑与数据持久化全流程。目前已有148人学习下载。项目主干版本结构清晰,可直接运行体验点餐、订单与用户认证等核心功能,帮助读者理解Kotlin与Java互操作、Android界面交互及服务端接口设计,是课程答辩与技能提升的实用参考。
1. 从一份大三课设拆起:Kotlin 外卖 App 的三层架构到底长什么样
很多人拿到「Kotlin 写的 Android 外卖应用,带数据库、客户端、服务端」这类压缩包,第一反应是解压、双击app-release.apk装上看看界面,然后就没有然后了。我这次拆的这份jleme课设包,价值不在那个能装到手机上的成品 APK,而在于它把移动开发里最容易讲糊的三层——客户端、服务端、数据库——用一套能跑通的代码串了起来。它适合两类人:正在做大三课程设计、需要一份结构完整参考的同学,以及想从「只会写 Activity」过渡到「理解一个 App 数据怎么从界面流到数据库再流回来」的 Android 初学者。包里除了 APK,还有gradlew.bat、index.css、饥了么外卖报告.docx和一张wel_jleme.gif启动图,说明它是一份带文档、带构建脚本、带演示素材的完整交付物,而不是一段孤零零的源码。下面我按「它是什么 → 怎么跑起来 → 坑在哪」的顺序,把这份资源拆到能照着复现。
2. 客户端、服务端、数据库:三层各自负责什么,为什么这么分
2.1 三层架构在课设里的真实分工
外卖类应用的本质是「一个用户看到菜单、下单、商家/后台处理订单」的闭环。这份课设把它拆成三层,不是照搬教科书,而是因为这三层的数据形态完全不同。客户端(Android 端)负责的是展示和交互,它拿到的是已经组装好的 JSON 或对象,不需要知道订单在数据库里是怎么存的;服务端负责业务规则,比如「下单时校验库存、生成订单号、计算总价」,它是唯一能直接碰数据库的角色;数据库负责持久化,用户表、菜品表、订单表、订单明细表都落在这里。
这么分的好处是:客户端换 UI 不用动服务端,服务端改业务逻辑不用重装 App,数据库换表结构只影响服务端。课设里常见的错误是把数据库操作直接写在 Activity 里,用SQLiteOpenHelper在客户端本地建表,结果「服务端」变成了一个空壳。这份包既然标题明确写了「包含数据库+客户端+服务端」,说明它是按真正的 C/S 结构组织的,客户端通过接口拿数据,而不是自己查本地库。
2.2 从压缩包文件看项目组成
先看包里能直接看到的文件,判断项目形态:
| 文件 | 类型 | 作用 |
|---|---|---|
app-release.apk/jleme.apk/jleme-v1.0.apk | 安装包 | 已编译的客户端成品,用于快速验证界面 |
gradlew.bat | 构建脚本 | Windows 下用 Gradle 编译项目的入口 |
index.css | 样式文件 | 服务端若带 Web 管理页,这是它的样式 |
饥了么外卖报告.docx | 文档 | 课程设计报告,含需求、设计、测试 |
wel_jleme.gif | 素材 | 启动页动画资源 |
gradlew.bat的存在说明客户端是标准 Android Gradle 工程,不是 Eclipse 时代的老结构。index.css是个关键信号——纯 Android 客户端项目不会带 CSS,它大概率对应服务端的一个后台管理页面,比如商家查看订单的 Web 端。这意味着这份课设的服务端可能不是纯 REST 接口,而是「接口 + 简易 Web 后台」的混合形态。
2.3 环境准备与工程导入
要复现,先备好环境。Android 端需要 Android Studio(建议 2022 以后的版本,对 Kotlin 支持更稳),JDK 用 11 或 17,Gradle 版本跟着工程里的 wrapper 走,不要手动改。服务端如果是 Java/Kotlin 写的,同样用 JDK 11+;如果带 Web 页面,可能还需要一个 Servlet 容器或 Spring Boot 内嵌容器。
导入步骤:
- 解压
jleme.zip,找到含settings.gradle或build.gradle的目录,那才是工程根目录,不要直接在压缩包外层用 Android Studio 打开。 - 用 Android Studio 的
Open选择工程根目录,等待 Gradle Sync。第一次 Sync 会下载依赖,网络不稳时容易卡在Downloading。 - Sync 成功后检查
local.properties里的sdk.dir是否指向本机 SDK,没有就手动补上。 - 服务端单独用 IDEA 打开,确认它的启动类和端口配置。
# Windows 下用工程自带的 wrapper 编译,避免本机 Gradle 版本不一致 gradlew.bat assembleDebug # 如果只想装到已连接的设备上 gradlew.bat installDebugassembleDebug生成的是调试包,installDebug会直接推到 adb 连接的设备。用 wrapper 而不是本机gradle命令,是因为 wrapper 锁定了工程验证过的 Gradle 版本,能避开「本机版本太新导致插件不兼容」这类玄学问题。
提示:如果 Sync 报
Could not find com.android.tools.build:gradle,多半是仓库地址被改过,检查build.gradle里的repositories是否包含google()和mavenCentral()。
3. 数据库设计与服务端接口:订单数据怎么从界面落到表里
3.1 核心表结构与字段设计
外卖系统的数据库设计有固定套路,核心就四张表:用户、菜品、订单、订单明细。下面是我按这类课设常见做法整理的表结构,字段名可按报告里的实际定义调整,但关系不能乱。
-- 用户表:存登录信息和角色 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 实际项目应存哈希,课设常存明文 phone VARCHAR(20), role TINYINT DEFAULT 0 -- 0 普通用户,1 商家/管理员 ); -- 菜品表:菜单数据 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, category VARCHAR(50), image_url VARCHAR(255) ); -- 订单表:一笔订单的主记录 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0 待处理 1 已完成 2 已取消 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ); -- 订单明细:一笔订单包含哪些菜品、各几份 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (dish_id) REFERENCES dish(id) );orders和order_item拆开是必须的,因为一笔订单有多个菜品,如果把菜品塞进订单表的一个字段里,查询和统计都会变成字符串拼接的灾难。status用整型而不是字符串,是为了索引效率和状态机判断方便。total_price冗余存在订单表里,是为了避免每次查订单都去order_item里求和,这是典型的读多写少场景下的取舍。
3.2 服务端接口与客户端调用
服务端对外暴露的接口通常按资源划分,客户端用 Retrofit 或 OkHttp 调用。下面是一个下单接口的服务端伪代码和客户端调用示例。
// 服务端:下单接口的核心逻辑(Kotlin + 任意 Web 框架) fun createOrder(userId: Int, items: List<OrderItemReq>): OrderResp { // 1. 校验库存 for (item in items) { val dish = dishDao.findById(item.dishId) ?: throw BizException("菜品不存在") if (dish.stock < item.quantity) { throw BizException("库存不足: ${dish.name}") } } // 2. 计算总价 var total = BigDecimal.ZERO for (item in items) { val dish = dishDao.findById(item.dishId)!! total = total.add(dish.price.multiply(BigDecimal(item.quantity))) } // 3. 写订单主表 + 明细表,扣库存(应在同一事务里) val orderId = orderDao.insert(userId, total) for (item in items) { orderItemDao.insert(orderId, item.dishId, item.quantity) dishDao.reduceStock(item.dishId, item.quantity) } return OrderResp(orderId, total, status = 0) }这段逻辑的关键在「校验—计算—写入」的顺序,以及第 3 步必须放在一个数据库事务里。如果扣库存和写订单分开提交,一旦中间失败,就会出现「库存扣了但订单没生成」的对不上账。课设里经常忽略事务,答辩时被问到就是硬伤。
// 客户端:Retrofit 接口定义与调用 interface ApiService { @POST("order/create") suspend fun createOrder(@Body req: CreateOrderReq): ApiResponse<OrderResp> } // 在 ViewModel 里调用 viewModelScope.launch { try { val resp = api.createOrder(req) if (resp.code == 0) { _uiState.value = UiState.Success(resp.data) } else { _uiState.value = UiState.Error(resp.msg) } } catch (e: Exception) { _uiState.value = UiState.Error("网络异常: ${e.message}") } }客户端用suspend函数配合viewModelScope,是为了不阻塞主线程。ApiResponse统一包一层code/msg/data,是服务端接口的常见约定,客户端只认code == 0为成功。这里要注意:createOrder是写操作,不能重试,网络超时后不能盲目重发,否则可能生成重复订单,正确做法是服务端用订单号做幂等。
3.3 客户端到服务端的地址配置
课设最容易翻车的地方是地址。模拟器访问本机服务端,localhost指向的是模拟器自己,不是你的电脑。
// 常见做法:把 baseUrl 抽到 BuildConfig 或常量里 object ApiConfig { // Android 模拟器访问宿主机用 10.0.2.2 const val BASE_URL = "http://10.0.2.2:8080/" // 真机调试时改成电脑的局域网 IP,如 http://192.168.1.100:8080/ }10.0.2.2是 Android 模拟器映射到宿主机的固定地址,真机则必须用电脑在局域网里的实际 IP,且手机和电脑要在同一网段。另外 Android 9 以后默认禁止明文 HTTP,需要在AndroidManifest.xml的application标签加android:usesCleartextTraffic="true",否则请求直接失败,日志里只有一句含糊的Cleartext HTTP traffic not permitted。
4. 避坑与排查:这份课设跑不起来时先看这几条
4.1 装上 APK 能看界面,但登录/下单全失败
现象:APK 能正常打开,点登录转圈后提示网络错误,或直接闪退。 原因:成品 APK 里打包的BASE_URL指向的是原作者当时的服务端地址,那个地址在你这里根本不存在,或者服务端压根没启动。 解决:不要指望成品 APK 能连上你的服务端。正确路径是导入源码,把BASE_URL改成你自己的地址,重新编译安装。服务端要先启动,确认http://你的IP:端口/在浏览器里能访问到接口返回,再让客户端去连。
4.2 Gradle Sync 卡住或报依赖找不到
现象:Android Studio 打开工程后一直Gradle: Resolve dependencies,或者报某个androidx库找不到。 原因:工程里的仓库配置可能只有jcenter(),而 jcenter 早已停服;或者依赖版本太老,新版本 Android Studio 不再默认支持。 解决:把build.gradle里的repositories改成google()+mavenCentral(),删掉jcenter()。如果某个依赖版本确实拉不到,去 Maven 仓库查一个相近的可用版本替换,改完点Sync Now。这一步没有后悔药,只能一个个依赖试。
4.3 数据库连不上或表不存在
现象:服务端启动报Unknown database 'jleme'或Table 'jleme.user' doesn't exist。 原因:只导入了代码,没有执行建库建表的 SQL;或者数据库连接配置里的库名、账号密码和本机不一致。 解决:先在 MySQL 里CREATE DATABASE jleme,再执行项目里的.sql脚本建表。然后检查服务端的数据库配置文件(常见是application.properties或db.properties),把url、username、password改成你本机的。MySQL 8 的连接串还要带serverTimezone=Asia/Shanghai,否则时间字段会报错。
4.4 图片加载不出来
现象:菜品列表文字正常,但图片全是占位图或空白。 原因:image_url存的是原作者的图床地址或本地路径,你的环境访问不到;或者客户端没申请网络权限。 解决:检查AndroidManifest.xml是否有<uses-permission android:name="android.permission.INTERNET"/>。图片地址如果是外链,换成你自己的图床或本地服务端静态资源路径。用 Glide 加载时打开日志,能看到具体是 404 还是超时。
4.5 中文乱码
现象:接口返回的中文在客户端显示成问号或方块。 原因:服务端响应头没指定 UTF-8,或数据库表和连接字符集不是utf8mb4。 解决:数据库建库时用CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,服务端响应设置Content-Type: application/json;charset=UTF-8。客户端 Retrofit 默认按 UTF-8 解析,一般不用改。
5. 进阶:把课设改成能写进简历的项目,以及我的验证习惯
课设能跑通只是及格线,要让这份jleme真正有价值,得做几处改造。第一,把明文密码换成 BCrypt 哈希,登录接口加一个简单的 Token 机制,哪怕只是 UUID 存内存,也比裸传密码强。第二,给下单接口加幂等键,客户端每次下单带一个唯一requestId,服务端用它去重,这样网络重试不会生成两笔订单。第三,把total_price的计算从服务端挪到数据库事务里用SELECT ... FOR UPDATE锁住菜品行,避免并发下单超卖。
验证改造是否生效,我一般会走一遍固定流程:
| 验证项 | 操作 | 预期结果 |
|---|---|---|
| 库存扣减 | 库存设 1,两个客户端同时下单同一菜品 | 只有一个成功,另一个提示库存不足 |
| 幂等 | 同一requestId连发两次下单请求 | 只生成一笔订单 |
| 密码安全 | 查数据库 user 表 | 密码字段是哈希值,不是明文 |
| 异常回滚 | 下单时故意让明细插入失败 | 订单主表和库存都不变 |
这套流程走下来,项目就从「能演示」变成了「能讲清楚边界」。我自己的习惯是:每次改完服务端逻辑,先不碰客户端,直接用 Postman 或 curl 打接口,确认返回符合预期,再去调 UI。因为客户端的问题往往只是表象,接口对了,UI 的问题才好定位。
# 用 curl 直接验证下单接口,绕开客户端 curl -X POST http://localhost:8080/order/create \ -H "Content-Type: application/json" \ -d '{"userId":1,"items":[{"dishId":1,"quantity":2}],"requestId":"test-001"}'这条命令的好处是把「客户端—网络—服务端—数据库」四段链路缩短成「curl—服务端—数据库」,一旦返回不对,问题一定在服务端或数据库,不用怀疑客户端。从那以后我每次调接口都强制先走一遍 curl,确认服务端没问题再动 Android 端,省下的排查时间比想象中多。希望这份拆解能帮你把这份课设真正跑起来、改下去。
本文还有配套的精品资源,点击获取