简介:这是一套基于 Android Studio 开发的前后台分离仓库管理系统完整源码,面向移动应用开发初学者、课程设计学生及需要 Android 实战练手项目的开发者。项目以角色权限为核心,划分超级管理员、商品管理员与出入库人员三类身份,覆盖注册登录、用户管理、商品增删查、入库出库等典型业务场景,能帮助读者理解多角色权限控制与前后台数据交互的完整实现思路。压缩包共 529 个文件,约 15.47MB,包含 18 个 java 源文件、35 个 xml 布局与配置、123 个 json 数据文件、16 张 png 及 5 张 jpg 界面素材,另有 dex、class、jar、gradle 等构建与依赖文件,结构清晰、注释详尽。项目涉及 ListView 列表、SQLite 数据库增删改查、下拉框与 Intent 传值等核心知识点,界面达十多个,UI 完成度较高。目前已有 3355 人学习,适合作为满分课设参考或 Android 入门进阶的实战范例。
1. 仓库管理系统课设:为什么前后台分离比堆UI更值得先做
做过 Android 课设的人都清楚一个尴尬现实:UI 做得再花哨,答辩老师一句「数据从哪来」就能把整个项目问穿。仓库管理系统这个题目尤其典型——入库、出库、库存查询、预警,四个模块背后是同一套数据在流动,如果全部塞进 Activity 里用 SQLite 硬扛,改一个字段就要动五六个文件。前后台分离的思路是:Android 端只负责界面渲染和用户交互,所有业务逻辑和数据持久化交给后台服务,两端通过 HTTP 接口通信。这样做的好处不是「显得高级」,而是当你需要把库存预警规则从「低于10件」改成「低于安全库存」时,只改后台一个方法,前端一行不动。这篇文章面向的是正在做课设、想把项目做得能讲清楚架构的 Android 初学者,也适合已经写完但说不清前后端边界的同学。接下来我会按「接口怎么定 → 前端怎么搭 → 后台怎么建 → 联调怎么排错」的顺序,把一套可复现的方案拆开讲。
2. 接口先行:仓库管理系统的 REST 接口设计与数据契约
2.1 为什么先定接口再写代码
前后台分离最容易翻车的地方不是技术难度,而是两端对「一个入库单长什么样」的理解不一致。前端以为quantity是整数,后台返回了字符串;前端按{code, data, msg}解析,后台直接返回了裸数组。这类问题在联调阶段暴露出来,改起来牵一发动全身。我的习惯是:动手写第一行 Kotlin 之前,先用一张表把接口定死,包括路径、方法、请求体字段、响应体字段、字段类型、是否必填。这张表就是两端的「合同」,后面谁改谁负责同步。
仓库管理系统的核心接口不多,通常六到八个就能覆盖课设需求:
| 接口路径 | 方法 | 请求参数 | 响应字段 | 说明 |
|---|---|---|---|---|
| /api/goods/list | GET | page, size, keyword | id, name, spec, quantity, safeStock | 分页查询商品 |
| /api/goods/add | POST | name, spec, quantity, safeStock | code, msg | 新增商品 |
| /api/stock/in | POST | goodsId, quantity, operator | code, msg | 入库 |
| /api/stock/out | POST | goodsId, quantity, operator | code, msg | 出库 |
| /api/stock/records | GET | goodsId, startTime, endTime | list[] | 出入库记录 |
| /api/warning/list | GET | 无 | list[] | 库存预警列表 |
这张表里有两个设计决策值得说清楚。第一,所有写操作统一返回{code, msg},code=0表示成功,非零表示业务失败(比如出库数量超过库存),HTTP 状态码始终是 200。这样做的好处是前端只需要判断code,不用同时处理 HTTP 异常和业务异常两套逻辑。第二,查询接口统一返回分页结构{code, data: {list, total}, msg},即使当前数据量很小,也预留分页字段,避免后期数据变多时改接口格式。
2.2 用 Postman 或 Apifox 先跑通接口再写前端
接口定完之后,不要急着打开 Android Studio。先用 Postman 或者 Apifox 把每个接口手动调一遍,确认后台返回的 JSON 结构和表格里写的一致。这一步花二十分钟,能省掉后面至少两小时的联调扯皮。具体做法是:在 Postman 里建一个 Collection,把六个接口全部录入,每个接口保存一个示例响应。这个 Collection 可以直接导出成 JSON 分享给前端同学(如果是团队课设),也可以作为自己写 Retrofit 接口定义时的参照。
调接口时重点看三件事:字段名大小写是否和约定一致、数值类型是否匹配(quantity是Int还是String)、空数据时返回的是null还是空数组。这三点是后面解析翻车的高发区。确认无误后,把示例响应复制到一个.json文件里,放在 Android 项目的assets目录下,写 Retrofit 的 Mock 拦截器时可以直接用,这样即使后台还没部署到服务器,前端也能先跑起来。
3. Android 端:用 Retrofit + RecyclerView 搭出能用的仓库管理界面
3.1 Gradle 依赖与 Retrofit 接口定义
Android 端的技术选型我一般固定为:Retrofit 做网络请求,Gson 做 JSON 解析,RecyclerView 做列表展示,ViewModel + LiveData 做数据持有。这套组合在课设场景下足够稳定,资料也多,遇到问题容易搜到答案。先在app/build.gradle里加依赖:
dependencies { implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' }版本号不用照抄,用 Android Studio 提示的稳定版即可。加完依赖后 Sync 一次,如果报Duplicate class错误,多半是某个库被间接引入了两次,用./gradlew app:dependencies查看依赖树,找到重复的用exclude排除。
接下来定义接口。假设后台部署在本机,模拟器访问本机用10.0.2.2,真机用电脑局域网 IP:
// ApiService.kt interface ApiService { @GET("api/goods/list") suspend fun getGoodsList( @Query("page") page: Int = 1, @Query("size") size: Int = 20, @Query("keyword") keyword: String? = null ): ApiResponse<PageData<Goods>> @POST("api/stock/in") suspend fun stockIn(@Body body: StockRequest): ApiResponse<Unit> @POST("api/stock/out") suspend fun stockOut(@Body body: StockRequest): ApiResponse<Unit> } // ApiResponse.kt 统一响应壳 data class ApiResponse<T>( val code: Int, val msg: String, val data: T? ) data class PageData<T>( val list: List<T>, val total: Int ) data class Goods( val id: Int, val name: String, val spec: String, val quantity: Int, val safeStock: Int ) data class StockRequest( val goodsId: Int, val quantity: Int, val operator: String )这里的关键点是ApiResponse<T>这个泛型壳。后台所有接口都返回{code, msg, data}三层结构,用泛型可以把data的类型交给调用方决定。suspend关键字让接口可以直接在协程里调用,不用再写enqueue回调。@Query用于 GET 参数,@Body用于 POST 的 JSON 体,Retrofit 会自动用 Gson 序列化。
3.2 RecyclerView 适配器与列表页实现
仓库管理系统的主界面通常是一个商品列表,每行显示名称、规格、库存数量,库存低于安全库存时数量标红。适配器写法:
class GoodsAdapter(private val onItemClick: (Goods) -> Unit) : RecyclerView.Adapter<GoodsAdapter.VH>() { private val items = mutableListOf<Goods>() fun submitList(newList: List<Goods>) { items.clear() items.addAll(newList) notifyDataSetChanged() } class VH(view: View) : RecyclerView.ViewHolder(view) { val tvName: TextView = view.findViewById(R.id.tv_name) val tvSpec: TextView = view.findViewById(R.id.tv_spec) val tvQty: TextView = view.findViewById(R.id.tv_qty) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_goods, parent, false) return VH(view) } override fun onBindViewHolder(holder: VH, position: Int) { val goods = items[position] holder.tvName.text = goods.name holder.tvSpec.text = goods.spec holder.tvQty.text = goods.quantity.toString() // 低于安全库存标红 val color = if (goods.quantity < goods.safeStock) { ContextCompat.getColor(holder.itemView.context, R.color.red_warning) } else { ContextCompat.getColor(holder.itemView.context, R.color.text_normal) } holder.tvQty.setTextColor(color) holder.itemView.setOnClickListener { onItemClick(goods) } } override fun getItemCount() = items.size }submitList方法里用notifyDataSetChanged是课设场景下的简化做法,数据量小的时候没问题。如果列表超过两百条出现滑动卡顿,换成DiffUtil做增量更新,只刷新变化的行。库存标红的逻辑放在onBindViewHolder里,每次绑定都重新判断,保证出库后列表刷新时颜色跟着变。
3.3 ViewModel 里发起请求与状态管理
Activity 里不直接调 Retrofit,而是通过 ViewModel 持有数据,这样旋转屏幕时数据不会丢:
class GoodsViewModel : ViewModel() { private val _goods = MutableLiveData<List<Goods>>() val goods: LiveData<List<Goods>> = _goods private val _error = MutableLiveData<String>() val error: LiveData<String> = _error fun loadGoods(keyword: String? = null) { viewModelScope.launch { try { val resp = RetrofitClient.api.getGoodsList(keyword = keyword) if (resp.code == 0) { _goods.value = resp.data?.list ?: emptyList() } else { _error.value = resp.msg } } catch (e: Exception) { _error.value = "网络异常:${e.message}" } } } }viewModelScope.launch确保协程在 ViewModel 销毁时自动取消,不会造成内存泄漏。try-catch捕获网络异常,把错误信息通过 LiveData 传给 Activity 弹 Toast。注意resp.data?.list用了安全调用,因为后台在查询无结果时data可能为null,直接.list会崩。
Activity 里观察 LiveData:
viewModel.goods.observe(this) { list -> adapter.submitList(list) } viewModel.error.observe(this) { msg -> Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() } viewModel.loadGoods()这套结构跑通后,入库和出库页面只需要复用同样的模式:一个表单、一个提交按钮、一个 ViewModel 方法。区别只是调用的接口不同。
4. 后台服务:用 Spring Boot + MySQL 实现库存业务逻辑
4.1 数据库表设计与库存扣减的原子性
后台选 Spring Boot 是因为它和 Android 端的 JSON 交互最顺,注解一贴就能跑。数据库用 MySQL,建两张核心表:
CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, spec VARCHAR(100), quantity INT NOT NULL DEFAULT 0, safe_stock INT NOT NULL DEFAULT 10 ); CREATE TABLE stock_record ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, type TINYINT NOT NULL COMMENT '1入库 2出库', quantity INT NOT NULL, operator VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (goods_id) REFERENCES goods(id) );出库操作有一个必须注意的点:扣减库存和写入记录必须在同一个事务里,否则可能出现库存扣了但记录没写,或者记录写了库存没扣。Service 层方法加@Transactional:
@Service public class StockService { @Autowired private GoodsMapper goodsMapper; @Autowired private StockRecordMapper recordMapper; @Transactional public void stockOut(int goodsId, int quantity, String operator) { Goods goods = goodsMapper.selectById(goodsId); if (goods == null) { throw new BizException("商品不存在"); } if (goods.getQuantity() < quantity) { throw new BizException("库存不足,当前库存:" + goods.getQuantity()); } // 扣减库存 goodsMapper.updateQuantity(goodsId, -quantity); // 写入出库记录 StockRecord record = new StockRecord(); record.setGoodsId(goodsId); record.setType(2); record.setQuantity(quantity); record.setOperator(operator); recordMapper.insert(record); } }@Transactional保证两个写操作要么都成功要么都回滚。库存不足时抛自定义异常BizException,由全局异常处理器捕获后返回{code: 1, msg: "库存不足..."},前端拿到code != 0就弹提示。这里不要用synchronized去锁方法,课设并发量低,数据库事务足够,加锁反而容易死锁。
4.2 统一响应封装与跨域配置
后台所有 Controller 返回统一格式,用一个Result类包一层:
public class Result<T> { private int code; private String msg; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 1; r.msg = msg; return r; } // getter/setter 省略 }Controller 里直接return Result.ok(goodsService.list())。全局异常处理器用@RestControllerAdvice捕获BizException并返回Result.fail(e.getMessage()),这样前端永远只需要解析一种结构。
跨域配置在课设阶段容易被忽略。如果 Android 端用模拟器访问本机后台,不涉及跨域;但如果用浏览器调试接口,或者后台部署到另一台机器,就需要加 CORS:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*"); } }allowedOrigins("*")在课设场景下够用,生产环境要改成具体域名。
4.3 库存预警的查询实现
预警列表的 SQL 很简单,就是查quantity < safe_stock的商品:
SELECT * FROM goods WHERE quantity < safe_stock ORDER BY (safe_stock - quantity) DESC;按差值降序排列,缺口最大的排最前面。MyBatis 里写一个对应的selectWarningList方法即可。这个接口不需要分页,因为预警商品通常不多。前端在首页顶部放一个「预警」入口,点进去展示这个列表,每条显示商品名和当前库存/安全库存的对比。
5. 联调避坑:六个让课设翻车的典型问题与排查路径
5.1 模拟器访问本机后台返回连接超时
现象:Postman 里接口正常,Android 模拟器里请求一直转圈最后超时。原因:模拟器里的localhost指向模拟器自身,不是宿主机。解决:把 BaseUrl 里的localhost或127.0.0.1改成10.0.2.2。真机调试则用电脑的局域网 IP(ipconfig查看),并确保手机和电脑在同一 WiFi 下。如果还不行,检查电脑防火墙是否拦截了 8080 端口。
5.2 Gson 解析报 IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY
现象:接口返回的 JSON 是数组,但 Retrofit 定义的是对象类型。原因:后台某个接口没有用Result包装,直接返回了List。解决:统一后台所有接口的返回格式,或者在 Retrofit 里把返回类型改成ApiResponse<List<Goods>>。我一般选前者,因为统一格式对前端更友好。
5.3 出库后列表数量没变,手动下拉才更新
现象:出库成功提示弹了,但列表里的库存数字还是旧的。原因:出库操作完成后没有重新调用loadGoods()。解决:在出库成功的回调里调一次viewModel.loadGoods(),或者在onResume里刷新。更优雅的做法是用ActivityResultLauncher,出库页面关闭时返回一个needRefresh标志,列表页收到后刷新。
5.4 库存扣成负数
现象:并发测试时(或者快速连点出库按钮)出现库存为负。原因:查询库存和扣减库存之间有间隙,两个请求同时读到相同库存。解决:在 SQL 的UPDATE语句里加条件AND quantity >= #{quantity},根据受影响行数判断是否成功:
UPDATE goods SET quantity = quantity - #{quantity} WHERE id = #{id} AND quantity >= #{quantity}Mapper 返回int,Service 里判断if (affected == 0) throw new BizException("库存不足")。这样即使并发也不会扣成负数。
5.5 Android Studio 报 Duplicate class 或资源重复
现象:Sync 或 Build 时报Duplicate class或Duplicate resources。原因:依赖冲突或res目录下有同名文件(比如两个ic_launcher.png在不同mipmap文件夹)。解决:依赖冲突用./gradlew app:dependencies查看树,找到重复的用exclude group: 'xxx'排除;资源重复检查res下各文件夹是否有同名文件,删掉多余的。
5.6 后台返回中文乱码
现象:前端收到的msg字段中文显示为问号或乱码。原因:后台响应头Content-Type没有指定 UTF-8。解决:在application.properties里加server.servlet.encoding.charset=UTF-8和server.servlet.encoding.force=true。如果是 Tomcat 部署,检查server.xml的Connector标签是否加了URIEncoding="UTF-8"。
6. 让课设经得起追问:接口文档、异常兜底与演示脚本
答辩时老师最爱问的三个问题是:「你这个数据存哪」「如果两个人同时出库怎么办」「断网了会怎样」。前两个在前面已经解决了,第三个需要在前端做兜底。我的习惯是在 ViewModel 的catch块里区分异常类型:SocketTimeoutException提示「连接超时,请检查网络」,ConnectException提示「无法连接服务器」,其他异常统一提示「操作失败,请重试」。这样演示时即使后台没启动,前端也不会直接崩,而是给出可读的提示。
另一个让课设加分的小技巧是准备一份接口文档。不需要多正式,用 Markdown 写一个表格,列出每个接口的路径、方法、参数、返回示例,放在项目根目录的docs/api.md里。答辩时如果老师问「前后台怎么约定的」,直接打开这个文件,比口头解释有说服力得多。文档里的返回示例直接从 Postman 里复制真实响应,不要手写,手写容易和实际不一致。
演示脚本我一般按这个顺序走:先展示商品列表(证明查询通),再新增一个商品(证明写入通),然后对这个商品做一次入库和一次出库(证明业务逻辑通),最后把它的库存改到低于安全库存,刷新看预警列表(证明预警通)。整个流程控制在三分钟内,每一步都有明确的「输入→输出」对应关系。演示前把数据库重置到初始状态,避免上次演示的脏数据干扰。
最后说一个我踩过的坑:不要在演示当天才把后台部署到服务器。课设环境里用本机跑后台 + 模拟器跑前端是最稳的组合,服务器部署涉及端口、防火墙、域名解析一堆变量,任何一个出问题都会耽误演示。如果必须远程演示,提前一天部署好并完整走一遍流程,把每一步的截图存下来,万一现场网络出问题可以切到截图讲解。希望帮到你。
本文还有配套的精品资源,点击获取