简介:这是一套基于 Android Studio 开发的前后台分离仓库管理系统完整源码,面向移动应用开发初学者、课程设计学生及需要安卓实战练手的人群。项目以角色权限为核心,划分超级管理员、商品管理员与出入库人员三类身份,覆盖注册登录、用户管理、商品增删查、入库出库等业务闭环,能帮助读者理解多角色权限控制与前后台数据交互的完整思路。压缩包共 529 个文件,约 15.47MB,包含 18 个 java 源文件、35 个 xml 布局、123 个 json 配置、28 个 dex 与 26 个 class 编译产物,以及 png、jpg 图片资源和 gradle 构建脚本,结构清晰、注释详尽。项目涉及 ListView 列表、SQLite 数据库增删改查、下拉框与 Intent 传值等安卓基础知识点,界面设计美观,适合直接作为课设参考或二次开发模板。目前已有 3355 人学习下载,配套博客提供项目介绍与运行演示,便于对照理解整体逻辑与实现细节。
1. 从课设到能交付:Android Studio 前后台分离的仓库管理系统到底怎么做
每年到了期末,问「Android Studio 实现前后台分离的仓库管理系统」的人就多起来。多数人的处境很具体:课设要求有客户端、有服务端、有数据库,UI 要好看,答辩要能演示,但自己只写过单机的增删改查,没碰过网络请求和接口设计。这个标题真正要解决的不是「写一个 App」,而是把仓库管理这个业务拆成两层——Android 端只负责界面和交互,服务端负责数据存储和业务规则,两边通过 HTTP 接口通信。适合谁?适合有 Java 或 Kotlin 基础、学过一点 Android 四大组件、但没做过完整前后端项目的人。做完之后你会拿到一个能跑通登录、入库、出库、库存查询的完整链路,而不是一个只能在本地点按钮的壳子。下面按我实际带学生做课设的顺序,把选型、接口、UI、打包和踩坑一次讲透。
2. 前后台分离的边界怎么划:接口先行,别让 Android 端碰数据库
前后台分离最容易翻车的地方,是边界没划清。很多人写着写着,Android 端直接连了 MySQL,或者把业务逻辑塞进了 Activity。这样做的后果是:换一台设备就连不上数据库,答辩时老师问「你的服务端做了什么」你答不上来。正确的做法是先定接口,再写两端。
2.1 为什么仓库管理系统必须做前后台分离
仓库管理的核心数据是库存,库存的特点是「多人同时操作、数据必须一致」。如果 Android 端直连数据库,两个客户端同时出库同一批货,就会出现超卖。前后台分离之后,所有写操作都经过服务端,服务端可以在一个事务里完成「查库存 → 判断是否足够 → 扣减 → 写流水」,这是单机 App 做不到的。
另一个现实原因是课设评分。老师看的是你有没有工程思维。前后台分离意味着你有接口文档、有分层、有异常处理,这些在答辩时都是加分项。UI 做得再花哨,如果架构是单机的,分数上不去。
从技术栈看,Android 端用 Retrofit + OkHttp 发请求,Gson 解析 JSON;服务端用 Spring Boot + MyBatis 或 JPA,数据库用 MySQL。这套组合资料多、坑少、答辩时老师也熟。不要为了炫技上 WebSocket 或 gRPC,课设场景用不上,反而增加调试成本。
2.2 接口设计:6 个接口撑起整个仓库业务
仓库管理系统的业务其实不复杂,核心就是「货、入、出、查」。我一般会先设计下面这组接口,用 RESTful 风格,返回统一 JSON 结构。
| 接口 | 方法 | 路径 | 作用 |
|---|---|---|---|
| 登录 | POST | /api/user/login | 校验账号密码,返回 token |
| 商品列表 | GET | /api/goods/list | 分页查询商品和库存 |
| 入库 | POST | /api/stock/in | 增加库存并写流水 |
| 出库 | POST | /api/stock/out | 扣减库存并写流水 |
| 流水查询 | GET | /api/record/list | 按时间/商品查出入库记录 |
| 商品新增 | POST | /api/goods/add | 新增商品档案 |
统一返回结构建议这样定:
{ "code": 200, "msg": "success", "data": {} }code用 200 表示成功,401 表示未登录,500 表示业务异常。Android 端只判断code,不解析 HTTP 状态码,这样两端约定清晰,调试时看日志一眼就能定位。
提示:接口路径和字段名一旦定下来,两端就按这个写,不要中途改。改一次字段名,Android 端和服务端都要动,课设时间紧,经不起折腾。
2.3 服务端最小实现:Spring Boot 三层结构
服务端不需要写得多复杂,Controller 接请求、Service 写业务、Mapper 操作数据库,三层够了。下面是一个入库接口的核心代码。
// StockController.java @RestController @RequestMapping("/api/stock") public class StockController { @Autowired private StockService stockService; @PostMapping("/in") public Result in(@RequestBody StockDTO dto) { // dto 包含 goodsId 和 count if (dto.getCount() <= 0) { return Result.fail("入库数量必须大于0"); } stockService.inStock(dto.getGoodsId(), dto.getCount()); return Result.success(); } }// StockService.java @Service public class StockService { @Autowired private GoodsMapper goodsMapper; @Autowired private RecordMapper recordMapper; @Transactional public void inStock(Long goodsId, Integer count) { // 先查商品是否存在 Goods goods = goodsMapper.selectById(goodsId); if (goods == null) { throw new BizException("商品不存在"); } // 增加库存 goodsMapper.addStock(goodsId, count); // 写流水 Record record = new Record(); record.setGoodsId(goodsId); record.setType("IN"); record.setCount(count); record.setCreateTime(new Date()); recordMapper.insert(record); } }逻辑说明:Controller 只做参数校验,不碰数据库;Service 上加@Transactional,保证「加库存」和「写流水」要么都成功要么都回滚。参数goodsId是商品主键,count是入库数量,必须大于 0。失败时抛BizException,由全局异常处理器统一转成Result.fail。
出库接口逻辑类似,区别是先判断库存是否足够:
@Transactional public void outStock(Long goodsId, Integer count) { Goods goods = goodsMapper.selectById(goodsId); if (goods == null) { throw new BizException("商品不存在"); } if (goods.getStock() < count) { throw new BizException("库存不足,当前库存:" + goods.getStock()); } goodsMapper.reduceStock(goodsId, count); // 写流水,type = OUT }这里有个细节:reduceStock的 SQL 要写成update goods set stock = stock - #{count} where id = #{id} and stock >= #{count},用数据库的行锁保证并发安全。如果只在 Java 里判断,两个请求同时进来还是会超卖。
2.4 Android 端网络层:Retrofit 封装与 token 携带
Android 端不要在每个 Activity 里写 OkHttp,统一封一个ApiClient。
// ApiClient.kt object ApiClient { private const val BASE_URL = "http://10.0.2.2:8080/" private val okHttpClient = OkHttpClient.Builder() .addInterceptor { chain -> val request = chain.request().newBuilder() .addHeader("token", SpUtil.getToken()) .build() chain.proceed(request) } .connectTimeout(10, TimeUnit.SECONDS) .build() val api: ApiService by lazy { Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(ApiService::class.java) } }参数说明:BASE_URL里的10.0.2.2是 Android 模拟器访问宿主机 localhost 的固定地址,真机调试要换成电脑的局域网 IP。addInterceptor里统一加 token,避免每个接口手动传。connectTimeout设 10 秒,太短容易误报超时,太长卡界面。
接口定义:
interface ApiService { @POST("api/user/login") suspend fun login(@Body body: LoginBody): Result<LoginData> @GET("api/goods/list") suspend fun goodsList(@Query("page") page: Int): Result<GoodsPage> }用 Kotlin 协程的suspend函数,配合lifecycleScope调用,不用手动切线程。如果项目是 Java 写的,用Call.enqueue回调也行,但代码会啰嗦一些。
注意:Android 9 以后默认禁止明文 HTTP,需要在
AndroidManifest.xml的<application>上加android:usesCleartextTraffic="true",否则请求直接失败,日志里报Cleartext HTTP traffic not permitted。这是新手最常卡住的地方。
3. 五星 UI 怎么落地:仓库管理系统的界面分层与控件选型
课设的 UI 要求通常是「好看、不卡、能演示」。好看不等于堆动画,仓库管理系统的界面核心是列表和表单,把这两类做干净,分数就不会低。
3.1 仓库管理系统的页面结构
一个完整的仓库管理 App 大概需要这些页面:
- 登录页:账号、密码、登录按钮
- 主页:底部导航,四个 Tab——库存、入库、出库、我的
- 库存列表页:搜索框 + RecyclerView + 分页加载
- 入库/出库页:商品选择 + 数量输入 + 提交按钮
- 流水页:按时间倒序的出入库记录
- 商品新增页:表单输入
页面不多,但每个页面都要处理「加载中、空数据、请求失败」三种状态。很多人只写了成功状态,答辩时网络一断就白屏,很尴尬。
3.2 用 Material Design 组件快速搭出规范界面
不要自己画背景和圆角,直接用 Material 组件库,风格统一还省时间。在build.gradle里加依赖:
implementation 'com.google.android.material:material:1.9.0'登录页用TextInputLayout包TextInputEditText,自带浮动标签和错误提示:
<com.google.android.material.textfield.TextInputLayout android:id="@+id/tilAccount" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="账号"> <com.google.android.material.textfield.TextInputEditText android:id="@+id/etAccount" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> </com.google.android.material.textfield.TextInputLayout>列表页用RecyclerView+MaterialCardView,每个商品一张卡片,显示名称、库存、单位。卡片比纯文字列表视觉层次好,实现成本也低。
<com.google.android.material.card.MaterialCardView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="8dp" app:cardCornerRadius="12dp" app:cardElevation="2dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp"> <TextView android:id="@+id/tvName" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textSize="16sp" android:textStyle="bold" /> <TextView android:id="@+id/tvStock" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/stock_color" /> </LinearLayout> </com.google.android.material.card.MaterialCardView>参数说明:cardCornerRadius控制圆角,12dp 是比较舒服的值;cardElevation控制阴影,2dp 足够,太大显得脏。库存数字用颜色区分,低于阈值显示红色,正常显示绿色,演示时老师一眼能看懂。
3.3 列表性能:RecyclerView 的三条优化习惯
UI 界面卡顿是热词里高频出现的问题,仓库管理系统列表数据一多就明显。三条习惯能解决大部分卡顿:
第一,onBindViewHolder里不做耗时操作。不要在里面查数据库、解析大 JSON、加载网络图片。数据在 ViewModel 里准备好,Adapter 只负责赋值。
第二,图片用 Glide 或 Coil 加载并设置缓存。商品图片如果直接setImageBitmap,滚动时会反复解码,必卡。
第三,分页加载用Paging 3或者自己监听RecyclerView滚动到底部再请求下一页,不要一次性拉全部数据。仓库商品上千条很常见,一次拉完内存扛不住。
// 监听滚动到底部 recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) { val lm = rv.layoutManager as LinearLayoutManager val lastVisible = lm.findLastVisibleItemPosition() if (lastVisible >= adapter.itemCount - 1 && !isLoading) { loadNextPage() } } })逻辑说明:findLastVisibleItemPosition拿到最后一个可见项的位置,接近总数时触发加载。isLoading是防抖标志,避免一次滚动触发多次请求。每页建议 20 条,太多单次响应慢,太少频繁请求。
3.4 状态管理:加载中、空数据、失败三态
每个网络请求页面都要有这三态。最简单的做法是在布局里叠一个FrameLayout,里面放ProgressBar、EmptyView、ErrorView,根据请求结果切换可见性。
fun showState(state: UiState) { when (state) { UiState.LOADING -> { progressBar.visibility = View.VISIBLE emptyView.visibility = View.GONE errorView.visibility = View.GONE } UiState.EMPTY -> { progressBar.visibility = View.GONE emptyView.visibility = View.VISIBLE } UiState.ERROR -> { progressBar.visibility = View.GONE errorView.visibility = View.VISIBLE } UiState.SUCCESS -> { progressBar.visibility = View.GONE emptyView.visibility = View.GONE errorView.visibility = View.GONE } } }参数说明:UiState是一个密封类或枚举,四个状态覆盖所有情况。错误页上放一个「重试」按钮,点击重新请求。这个细节在答辩演示时很加分,因为老师会故意断网看你有没有处理。
4. 避坑与排查:从连不上服务端到打包失败的 5 个血泪记录
这一章是我带课设时被问得最多的问题,每条都按「现象 → 原因 → 解决」写,照着排查基本能覆盖 90% 的翻车场景。
4.1 模拟器请求 localhost 一直超时
现象:服务端在电脑上跑着,浏览器能访问,但 Android 模拟器里请求一直转圈,最后报ConnectException。
原因:模拟器里的localhost指向模拟器自己,不是你的电脑。这是网络命名空间隔离导致的,不是代码问题。
解决:把BASE_URL里的localhost或127.0.0.1换成10.0.2.2。如果是真机,换成电脑的局域网 IP,并确保手机和电脑在同一个 WiFi 下。另外检查电脑防火墙有没有拦 8080 端口,Windows 上经常是防火墙的锅。
4.2 明文 HTTP 请求被系统拦截
现象:请求发出去了,但直接失败,Logcat 里看到Cleartext HTTP traffic to 10.0.2.2 not permitted。
原因:Android 9(API 28)开始默认禁止明文 HTTP,只允许 HTTPS。课设服务端一般没配证书,用的就是 HTTP。
解决:在AndroidManifest.xml的<application>标签加android:usesCleartextTraffic="true"。如果只想对特定域名放开,用network_security_config.xml配置白名单,更规范。
<application android:usesCleartextTraffic="true" ...>注意:这个配置只适合开发和课设,正式上线必须用 HTTPS。答辩时如果老师问起,要能说出这个区别。
4.3 主线程做网络请求导致 ANR
现象:点击登录按钮后界面卡死几秒,然后弹「应用无响应」。
原因:在onClick里直接调了同步网络请求,主线程被阻塞。Android 主线程超过 5 秒无响应就 ANR。
解决:用协程或线程池把网络请求放到后台。Kotlin 里用lifecycleScope.launch加withContext(Dispatchers.IO):
lifecycleScope.launch { val result = withContext(Dispatchers.IO) { ApiClient.api.login(LoginBody(account, password)) } // 这里回到主线程更新 UI if (result.code == 200) { startActivity(Intent(this@LoginActivity, MainActivity::class.java)) } else { Toast.makeText(this@LoginActivity, result.msg, Toast.LENGTH_SHORT).show() } }参数说明:Dispatchers.IO是 IO 密集型线程池,适合网络和数据库操作。withContext执行完自动切回主线程,不用手动runOnUiThread。
4.4 Gradle 同步失败或依赖下载不下来
现象:打开项目后 Gradle 一直转圈,或者报Could not resolve com.google.android.material:material:1.9.0。
原因:网络问题导致依赖下载失败,或者 Gradle 版本和 Android Gradle Plugin 版本不匹配。
解决:先检查gradle-wrapper.properties里的 Gradle 版本和build.gradle里的 AGP 版本是否对应。国内网络建议在settings.gradle里配置镜像仓库:
dependencyResolutionManagement { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } google() mavenCentral() } }如果还是失败,删掉项目根目录的.gradle和build文件夹重新同步。这个操作相当于「后悔药」,能解决大部分缓存导致的玄学问题。
4.5 打包 APK 后安装失败或闪退
现象:Debug 运行正常,打 Release 包安装到手机上闪退。
原因:最常见的是混淆规则没配,Retrofit 和 Gson 的类被混淆后反射失败。其次是签名配置不对。
解决:在proguard-rules.pro里加保留规则:
-keep class com.example.warehouse.model.** { *; } -keepattributes Signature -keepattributes *Annotation* -dontwarn okhttp3.** -dontwarn retrofit2.**参数说明:-keep class ...model.**保留数据模型类,Gson 反射要用;-keepattributes Signature保留泛型信息,Retrofit 解析返回值要用。打包前先用./gradlew assembleRelease命令行打一次,看有没有报错,比在 IDE 里点按钮更容易看到完整日志。
5. 让课设多拿两分的三个进阶技巧
前面把主链路讲完了,这一章说三个能让你的仓库管理系统在答辩时明显区别于其他人的做法。不是花架子,都是能实际跑起来、老师一问就能答出原理的东西。
5.1 用拦截器做统一错误处理
现在每个请求回来都要判断code,代码重复。可以在 OkHttp 拦截器里统一处理 401 跳登录、500 弹提示,业务层只处理成功数据。
class ErrorInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(chain.request()) if (response.code == 401) { // 切主线程跳登录 Handler(Looper.getMainLooper()).post { AppContext.instance.startActivity( Intent(AppContext.instance, LoginActivity::class.java) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ) } } return response } }逻辑说明:拦截器在网络层统一处理,业务代码不用再写if (code == 401)。AppContext是一个全局 Application 引用,用来在非 Activity 环境启动 Activity。注意加FLAG_ACTIVITY_NEW_TASK,否则会崩。
5.2 库存变更用本地广播通知列表刷新
入库或出库成功后,库存列表页需要刷新。如果每次回到列表页都重新请求,体验差还费流量。用LiveData或EventBus做一次通知,列表页收到后局部更新。
// 在入库成功后发送 StockEventBus.stockChanged.postValue(true) // 列表页监听 StockEventBus.stockChanged.observe(this) { changed -> if (changed == true) { loadGoodsList() } }参数说明:StockEventBus是一个单例对象,里面放MutableLiveData。用LiveData而不是EventBus是因为它生命周期安全,页面销毁后不会回调。这个技巧在答辩时讲出来,能体现你对 Android 架构的理解。
5.3 用真机 + 局域网做最终演示
模拟器演示有两个风险:一是性能差,列表滚动卡顿明显;二是老师可能让你现场装 APK,模拟器装不了。我的习惯是提前用真机连电脑局域网调试一遍,把BASE_URL改成电脑 IP,确认登录、入库、出库全流程通畅。演示当天用手机热点组网,电脑和手机连同一个热点,不依赖教室 WiFi。
最后说个我自己的教训:第一次带课设时,我把所有代码写在一个 Activity 里,答辩时老师问「如果我要加一个盘点功能,你改哪里」,我答不上来。后来重构成前后台分离 + 分层,同样的功能加一个接口、一个页面就行。结构清晰不是为了好看,是为了让你在答辩时能说清楚每一层在干什么。希望帮到你。
本文还有配套的精品资源,点击获取