简介一套基于Android Studio实现前后台分离的仓库管理系统完整源码项目面向移动应用开发初学者、课程设计学生及需要参考完整Android项目的开发者。系统按角色划分超级管理员、出入库人员和商品管理员覆盖注册登录、用户管理、商品增删查、入库出库等十多个界面运用ListView列表、SQLite数据库增删改查、下拉框、Intent传值等典型技术代码注释详细、逻辑清晰易于理解和二次开发。资源包为zip压缩包共529个文件包含Java源码、XML布局、Gradle配置、APK安装包、JAR类库、PNG图标等整体约15.47MB可直接导入Android Studio运行。目前已有3355人浏览学习适用于毕业设计或课程设计场景。通过这套完整项目可快速掌握多角色权限判断、数据库操作与界面交互的完整实现思路配套博客提供了项目介绍和运行演示可边读代码边对照效果帮助初学者避开常见配置雷区提升Android项目实战能力。1. 前后台分离的仓库管理系统课设答辩开场就问“后台在哪”做课程设计的时候最怕答辩老师问一句“你的项目是单机还是前后台分离”——前者意味着你只是写了个本地Demo后者才像一个能交付的系统。很多人在 Android Studio 里做仓库管理系统一个App直连SQLite功能扒拉完界面也很炫但这个问题一出来就露馅。我见过不少课设分数拉开差距的不是功能多少而是架构是不是真的前后台分离。这篇按我的实际落地经验用 Android Studio 做前台、一套轻量HTTP服务做后台把仓库管理系统的登录、库存、进销存、UI、联调和答辩演示一次拆开讲小白一步步跟着走也能做到五星UI、答辩不慌。2. 把仓库管理系统拆成前台App和后台服务接口怎么约定前后台分离落到仓库管理系统里含义很直接Android App 只负责展示和收集操作所有针对数据库的读写都放到后台服务里。App 通过 HTTP 接口把命令发给后台后台执行完把结果原路返回。这样做的直接好处是换手机、换模拟器数据不丢多个人同时访问库存不会各看各的答辩时还能现场演示“后台改数据、App跟着变”的效果这是单机版做不到的。2.1 为什么课设选前后台分离而不是单机SQLite先说你最常想到的替代方案Android 端直接用 SQLiteOpenHelper 或 Room自己建表自己查。开发确实快两三天就能把增删改查跑通但把它当成课设交上去几个硬伤摆在面前。首先是数据隔离模拟器里辛苦录入的几十条商品把项目拷贝到另一台电脑上SQLite 文件没同步过来演示时只能临时录数据画面很难看。其次是场景问题仓库管理系统天然是多角色、多终端使用的场景进货员要录单仓管要查库存老板要看统计一个单机 App 撑不起这个结构。最后是答辩层面只要老师追问“你的数据存在哪、能不能多台设备同时用”单机版就会变成扣分项。前后台分离正好把这些问题转成加分项。Android 端因为不用碰数据库代码量会明显减少网络层、ViewModel、UI 三层分开写课设报告也容易组织后台可以做成一个独立的小服务用 Spring Boot 或者更轻的 Node.js 都可以。我建议小白优先看 Spring Boot原因是网上的课设资料和 Demo 多出了问题好搜。如果你的电脑配置一般或者不想装 MySQL后台数据库换成 SQLite 也完全能演示接口和架构不用动。2.2 后台最小工程结构Controller、Service、Mapper三层后台服务不需要做得多大能跑、能连数据库、能应答接口就够了。我一般会按下面的目录组织这几乎是把正式项目的结构缩到最小warehouse-server/ ├── src/main/java/com/example/warehouse │ ├── controller/ # 接收HTTP请求不做业务 │ ├── service/ # 业务逻辑事务在这里 │ ├── mapper/ # 数据库访问SQL或注解 │ ├── model/ # 实体类、请求体、返回体 │ └── config/ # 跨域、拦截器、统一返回 ├── src/main/resources │ ├── application.yml # 端口、数据库连接 │ └── schema.sql # 建表语句方便重建 └── pom.xmlController 只做参数校验和结果包装Service 里放“先查库存、再扣减”这类逻辑Mapper 管 SQL。这样前后台分离不仅体现在 App 和后台之间也体现在后台内部。写课设文档的时候一定要解释清楚这个分层它往往比代码本身更能让老师相信“这不是网上随便拉的 Demo”。在 application.yml 里最需要改的就两三个参数端口默认 8080MySQL 连接串一定带 useUnicodetruecharacterEncodingutf-8MyBatis 的 map-underscore-to-camel-case 设为 true。服务起来之后先通过浏览器访问后台的任意一个 GET 接口确认后台和数据库真的通了再去写 Android 端顺序不能反。2.3 登录接口与token前后台第一次握手仓库管理系统的第一个接口我建议不是获取商品列表而是登录。登录接口能跑通说明网络、JSON 解析、返回体封装这一整条链路都通了。约定好接口的返回格式后面的库存接口全部复用减少重复代码。我常用的返回体长这样public class ResultT { private Integer code; // 0成功400参数错误401未登录 private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.msg ok; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }这个类的意义是给 Android 端一个统一的解析格式。code 为 0 走成功分支非 0 直接弹 msgAndroid 端只需要判断一次不用每个接口各自解析。返回体统一之后前端写一个 BaseObserver 或者协程封装就能应付所有接口。登录接口的业务做得很薄拿用户名密码查库匹配上了生成一个 token匹配不上返回 401。这里有个课设常用的简化写法token 先放在一个线程安全的 Map 里而不急着上 Redis。PostMapping(/login) public ResultString login(RequestBody LoginBody body) { String username body.getUsername(); String password body.getPassword(); if (username null || password null) { return Result.error(400, 用户名或密码不能为空); } User user userMapper.findByUsername(username); if (user null || !user.getPassword().equals(password)) { return Result.error(401, 用户名或密码错误); } String token UUID.randomUUID().toString().replace(-, ); tokenStore.put(token, user.getId()); return Result.success(token); }这里有几个参数层面的注意点。第一登录用 POST 而不用 GET用户名密码放在请求体里而不是拼在 URL 上避免 URL 被日志打出来第二密码比较在课设阶段可以直接比对明文但要在报告里写明生产环境必须加盐哈希老师看到这一句会认为你考虑过安全问题第三token 要设置有有效期2 小时过期后台 Map 里加一个过期时间字段定期清理。把这段逻辑讲清楚比堆功能更见功底。Android 端拿到 token 之后要把它存到 SharedPreferences 里后续每次请求在 header 里带 Authorization: token。后台用一个拦截器统一校验超过 2 小时返回 401App 检测到 401 就跳回登录页。这就是一个最小但完整的前后台握手流程。2.4 Android端网络层搭建Retrofit与协程怎么配合Android 这一侧网络库我用的是 Retrofit 加 OkHttp 的组合这是目前教程最多的搭配。依赖加两三条就够不需要额外引入 RxJava小白用协程反而更好理解。object ApiClient { private const val BASE_URL http://10.0.2.2:8080/ private val okHttpClient OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(15, TimeUnit.SECONDS) .addInterceptor { chain - val token TokenManager.getToken() if (token.isNullOrEmpty()) { chain.proceed(chain.request()) } else { val newRequest chain.request().newBuilder() .header(Authorization, token) .build() chain.proceed(newRequest) } } .build() val api: StockApi Retrofit.Builder() .baseUrl(BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(StockApi::class.java) }这里最需要注意的就是 BASE_URL后面避坑章会专门展开。先记一句话Android Studio 自带模拟器访问电脑本机后台要用 10.0.2.2不能用 localhost。超时时间我习惯给连接 10 秒、读取 15 秒仓库管理系统的接口体量小这个参数够用又不至于让用户等太久。接口定义用 Kotlin 的 suspend 函数写法比 Call 回调清爽得多interface StockApi { POST(api/user/login) suspend fun login(Body body: LoginBody): ResponseResultLoginData }在 LoginViewModel 里发请求时viewModelScope.launch 配合 try/catch成功失败分别改 LiveData 状态fun login(username: String, password: String) { viewModelScope.launch { _uiState.value LoginUiState.Loading try { val resp ApiClient.api.login(LoginBody(username, password)) if (resp.isSuccessful) { val body resp.body() if (body?.code 0) { TokenManager.save(body.data.token) _uiState.value LoginUiState.Success } else { _uiState.value LoginUiState.Error(body?.msg ?: 登录失败) } } else { _uiState.value LoginUiState.Error(网络异常 ${resp.code()}) } } catch (e: Exception) { _uiState.value LoginUiState.Error(e.message ?: 网络不可用) } } }这一段要解释三个点。第一viewModelScope.launch 默认在主线程协程里执行里面调用 suspend 挂起函数网络请求会自动切到 IO 线程不会再触发 NetworkOnMainThreadException第二Response.isSuccessful 只代表 HTTP 状态码是 2xx业务上的失败要靠 body.code 判断这两层判断不能省第三把 UI 状态用一个密封类或枚举表达Loading、Success、Error 分开界面层很难出现“登录成功但没跳转”的混乱状态。到这里前后台分离的技术骨架已经搭完下一步是往里面填仓库的业务功能。3. 核心业务落地入库、出库、库存查询怎么一次打通仓库管理系统最常见的功能就是进货入库、销售出库、库存查询、库存预警。很多课设把“操作日志”和“库存表”分开设计这没有错但我会用一张流水表加一张库存表来完成。原因是库存是一个随时变化的状态流水是每一次变化的证据把证据和状态分开出库错了才能查得回来。3.1 数据表设计库存表跟流水表怎么搭配先给两张核心表的建表 SQL直接在后台的 schema.sql 里执行就行。字符集必须用 utf8mb4这是为后面可能遇到的中文乱码提前吃后悔药。CREATE TABLE goods ( id BIGINT AUTO_INCREMENT PRIMARY KEY, sku_code VARCHAR(32) NOT NULL COMMENT 商品编码, name VARCHAR(64) NOT NULL COMMENT 商品名称, category VARCHAR(32) COMMENT 分类, unit VARCHAR(16) DEFAULT 件 COMMENT 单位, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, safe_stock INT NOT NULL DEFAULT 0 COMMENT 安全库存线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_sku (sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; CREATE TABLE stock_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, goods_id BIGINT NOT NULL COMMENT 商品ID, change_type TINYINT NOT NULL COMMENT 1入库 2出库 3盘点, change_qty INT NOT NULL COMMENT 变动数量正数, before_stock INT NOT NULL, after_stock INT NOT NULL, remark VARCHAR(128), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_goods_time (goods_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;字段设计上有几个注意点。库存字段用 int课设规模完全够用before_stock 和 after_stock 都记录下来以后做统计报表可以直接算不用反推查询入口是商品编码所以 sku_code 加唯一键其余字段不要乱加索引写起来会更慢。另外我没有建外键。很多课设喜欢在 SQL 里写 FOREIGN KEY但实际业务里外键容易造成锁竞争这里用应用层逻辑保证一致性报告中用一句“外键约束由 Service 层保证”带过就行。3.2 入库和出库同一个接口变化量有正负入库和出库在后台代码里几乎是同一套逻辑锁住库存记录、算新库存、写流水、更新库存。我习惯把它们合并成一个 changeType 参数传递而不是写两个 Controller 方法。入库传 1出库传 2。Transactional public StockRecordVO stockChange(StockChangeDTO dto) { Goods goods goodsMapper.selectBySku(dto.getSkuCode()); if (goods null) { throw new BizException(商品编码不存在); } int delta dto.getChangeType() 1 ? dto.getChangeQty() : -dto.getChangeQty(); int after goods.getStock() delta; if (after 0) { throw new BizException(库存不足); } StockRecord record new StockRecord(); record.setGoodsId(goods.getId()); record.setChangeType(dto.getChangeType()); record.setChangeQty(dto.getChangeQty()); record.setBeforeStock(goods.getStock()); record.setAfterStock(after); record.setRemark(dto.getRemark()); stockRecordMapper.insert(record); Goods update new Goods(); update.setId(goods.getId()); update.setStock(after); goodsMapper.updateStock(update); return convert(record); }这段代码在课设里属于“看起来不复杂但逻辑完整”的加分点。Transactional 保证流水写入和库存更新要么都成功要么都失败先查一次商品再更新是为了拿到 before_stock出库时判断 after 0直接抛业务异常不会出现库存变成负数。需要说明的是这里没有加数据库行锁因为小白阶段用行锁容易把自己锁死可以在报告里留一句“后续可以升级为 SELECT FOR UPDATE”。真实的库存系统必须考虑并发扣减答辩问到这一层时你能说出这个演进方向分数不会低。Android 端调用这个接口同样很简洁。界面层在 ViewModel 里发请求成功后不是直接改本地数值而是重新拉一次库存列表保证看到的是后台算出来的最新值fun stockIn(skuCode: String, qty: Int) { viewModelScope.launch { try { val resp ApiClient.api.stockChange( StockChangeDTO(skuCode, 1, qty, 手动入库) ) if (resp.body()?.code 0) { loadList() } else { _uiState.value ListUiState.Error(resp.body()?.msg ?: 入库失败) } } catch (e: Exception) { _uiState.value ListUiState.Error(e.message ?: 网络不可用) } } }注意这里把后台返回的业务提示直接透传到界面“商品编码不存在”就原样显示不要自己在 Android 端再猜一套错误文案否则后台改了一条业务规则前端文案就对不上了。两端的错误提示字典应当是一套。3.3 库存列表分页与条件搜索一个接口两种常规坑库存商品列表大概率要支持按商品名称搜索、按分类筛选、分页加载。后端接口设计成两个参数就够了keyword 是模糊搜索词page 是页码public PageResultGoodsVO pageGoods(int page, int size, String keyword, String category) { PageGoods p new Page(page, size); LambdaQueryWrapperGoods qw new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { qw.like(Goods::getName, keyword).or().like(Goods::getSkuCode, keyword); } if (StringUtils.hasText(category)) { qw.eq(Goods::getCategory, category); } qw.orderByDesc(Goods::getUpdateTime); goodsMapper.selectPage(p, qw); return new PageResult(p.getRecords(), p.getTotal()); }这里有两个课设里极易踩的坑。第一or 和 like 混用时会破坏条件组合上面代码的写法在 keyword 和 category 都存在时“关键词A or 编码B and 分类C”的语义会跟你预想的不一样最稳的写法是把 or 条件包在 and 里或者直接用两条 like 查询。第二分页的 page 参数要前后端对齐我统一用第一页传 1、每页 20 条RecyclerView 的加载更多拿到的是下一页页码。注意分页的 page 与 size 要在接口文档里固定下来前端第一页传 1每页 20 条后端不要默认从 0 开始否则和前端对不上列表会少一页。库存预警也放在列表接口里更省事。后台返回 goods 时带一个 lowStock 字段逻辑是 stock safe_stock 就标记为 trueAndroid 端在 Adapter 里对预警行换一个浅黄色背景。不需要单独再开一个接口演示时列表里直接能看到“预警”标签比让老师手动点进详情页更有展示效果。4. 五星UI的落地做法从视觉规范到布局细节“五星UI”听起来很玄学拆开看就是三件事颜色统一、层次清楚、状态完整。很多同学把界面做得花里胡哨红黄蓝全上反而让老师觉得是素材模板。真正能拿高分的 UI是那种截图发到群里没人猜得到是课设的界面。4.1 先定视觉规范再写布局颜色、字号、间距统一做 Android 课设我建议先把 colors.xml 写明白再开始画界面。那些“一眼看上去很专业”的 Demo配色通常不超过三种一个主色、一个中性背景色、一个警示色。主色用于按钮、选中态和数字卡片背景用偏灰的浅色而不是纯白警示色只用在库存预警这类真正需要提醒的地方。resources color namecolorPrimary#2D6A4F/color color namecolorPrimaryDark#1B4332/color color namecolorAccent#F4A261/color color namebgPage#F5F6F8/color color nametextPrimary#212121/color color nametextSecondary#757575/color color namedanger#D64545/color /resources这里特意不用 Material 默认的蓝紫色换成一个偏仓储、偏稳重的墨绿色视觉上和普通课设拉开了距离。主色选定之后按钮的 normal 状态、按压状态、数字卡片的高亮都从这一组颜色里取不要在某一个页面里重新发明一套配色。间距也一样页面左右统一 16dp卡片间距统一 12dp列表行高统一 72dp整份代码看起来就像同一套体系出来的。这套东西写进课设文档里就叫“视觉规范”老师看到这个词就知道你有 UI 设计意识。4.2 首页仪表盘四张数字卡片怎么做出“贵”的感觉仓库管理系统的首页通常是仪表盘库存商品总数、今日入库、今日出库、低库存预警。这个页面是答辩时的第一屏也是五星UI里最容易被截图的部分。我用 LinearLayout 加 layout_weight 平分做两列卡片因为卡片数量固定不需要网格布局的灵活性。com.google.android.material.card.MaterialCardView android:layout_width0dp android:layout_heightwrap_content android:layout_weight1 app:cardCornerRadius16dp app:cardElevation2dp app:strokeWidth0dp app:rippleColor#20000000 LinearLayout android:layout_widthmatch_parent android:layout_heightwrap_content android:orientationvertical android:padding16dp TextView android:idid/tvStockTotal android:layout_widthwrap_content android:layout_heightwrap_content android:text-- android:textColorcolor/colorPrimary android:textSize28sp android:textStylebold / TextView android:layout_widthwrap_content android:layout_heightwrap_content android:layout_marginTop6dp android:text库存商品总数 android:textColorcolor/textSecondary android:textSize13sp / /LinearLayout /com.google.android.material.card.MaterialCardViewcardCornerRadius 给 16dp配合浅色背景会有“圆润但不卡通”的效果cardElevation 只给 2dp阴影是 UI 的调味料给多了整页发灰还会触发过度绘制strokeWidth 设为 0不要描边让卡片和背景靠色差与阴影分层。卡片里的数字用 28sp 加粗主色着色标签用 13sp 次级文字色数字和标签之间留 6dp这个间距让信息层级一目了然。这组参数组合起来比单纯放一个 TextView 加背景色要耐看很多。首页第二屏展示最近 N 条流水用简单 item 的 RecyclerView 即可不要在一屏里塞太多入口。课设里“功能全”通过列表体现不要通过堆按钮体现后者只会让用户视觉疲劳。4.3 列表、加载、空状态评委都看这三个细节功能列表页的细节决定了使用体验是不是“半成品”。常见的翻车现场是数据没加载出来时页面白屏数据为空时页面白屏加载失败时还是白屏。白屏是课设 UI 最大的坑原因是只写了 Adapter 填充数据没有处理 loading、empty、error 三个状态。我习惯在一个 Fragment 里用三块视图轮换最外层是 FrameLayoutFrameLayout ... androidx.recyclerview.widget.RecyclerView android:idid/rvList android:layout_widthmatch_parent android:layout_heightmatch_parent / com.google.android.material.progressindicator.LinearProgressIndicator android:idid/loading android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_gravitytop android:indeterminatetrue / LinearLayout android:idid/emptyView android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter android:orientationvertical android:visibilitygone TextView android:layout_widthwrap_content android:layout_heightwrap_content android:text暂无数据试试右上角入库 / /LinearLayout /FrameLayoutloading 用顶部细进度条而不是全屏 Dialog全屏 Dialog 会阻断操作演示时还会让人觉得卡死。emptyView 里的文案要具体写“暂无数据试试右上角入库”比“暂无数据”更有引导性。网络错误时切一个带重试按钮的 errorView点击重试再调一次 loadList。这三态切换逻辑放在 ViewModel 的 uiState 里驱动哪一屏可见由状态决定。列表 item 高度控制在 72dp 左右内容放商品名加编码、一行分类、右侧库存数字。超过三行信息的 item 会让列表显得凌乱。库存预警行用浅黄背景加一个“预警”小标签比弹窗提醒更直观。做完这些再打开开发者选项里的“调试 GPU 过度绘制”如果页面显示粉红或红色就要检查是不是阴影叠加过多、背景重复绘制这也是“ui界面卡顿”最常见的元凶。5. 前后台联调避坑5个让小白翻车的现场前后台分离的课设代码写完只算一半联调跑通才算完。这一章把我见过的几个高频翻车现场按现象、原因、解决写清楚你照着排查能省下半天时间。5.1 模拟器连不上本地后台10.0.2.2还是localhost现象点击登录后马上提示“网络异常”Logcat 里报 Failed to connect to /127.0.0.1:8080但电脑浏览器访问后台一切正常。原因Android Studio 自带模拟器有一套自己的网络栈它把宿主机映射成了 10.0.2.2。你在模拟器里写 localhost访问的是模拟器自己不是电脑上的后台服务。解决BASE_URL 统一使用 http://10.0.2.2:8080/。如果用的是第三方模拟器网络映射规则不一定相同先在模拟器浏览器里访问 http://10.0.2.2:8080/ 探一下通了再填回代码。真机调试要填电脑的局域网 IP而且手机和电脑必须同一个 Wi-Fi电脑防火墙要放行 8080 端口。最省心的做法是把 BASE_URL 定义成 BuildConfig 字段Debug 包用 10.0.2.2Release 包留一个可替换的域名常量避免每次联调都改代码。5.2 主线程网络请求ANR和NetworkOnMainThreadException现象App 启动后点击登录界面卡住几秒后弹出“无响应”或者 Logcat 直接报 NetworkOnMainThreadException。原因老写法是在 onClick 里直接写 OkHttp 的 execute 方法这是同步请求网络 IO 占住了主线程。主线程被 IO 阻塞系统认为应用卡死了。解决不要用同步 execute用 Retrofit 的 suspend 函数配 viewModelScope.launch或者用 OkHttp 的 enqueue 异步回调。核心原则是主线程只更新 UI网络访问放到 IO 线程。还要注意子线程不能直接更新 TextView回调里要做线程切换。用协程的 withContext(Dispatchers.Main) 或者 LiveData.postValue 都可以这也是前面例子里把所有结果都通过 LiveData 抛给 UI 层的原因。5.3 Android 13上onBackPressed失效现象在 Android 13 或 14 的模拟器上运行按返回键没有进入自定义的双击退出逻辑Activity 直接关了或者根本不响应。原因API 33 把 Activity 的 onBackPressed 方法标记为废弃系统改用了 OnBackPressedDispatcher 回调机制。原来重写的 onBackPressed 在新设备上不再被调用。解决换成 OnBackPressedCallback 注册onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (isTaskRoot()) { val dlg AlertDialog.Builder(thisMainActivity) .setTitle(提示) .setMessage(确认退出系统) .setPositiveButton(退出) { _, _ - finish() } .setNegativeButton(取消, null) .create() dlg.show() } else { finish() } } })这段代码要放在 setContentView 之后。OnBackPressedCallback 构造函数的第一个参数是 enabled可以在登录成功前把它设为 false禁止返回键跳过登录页。这个坑在课设里不常被注意到但新版本模拟器默认就是 Android 13 以上遇到概率不低。5.4 中文乱码数据库、连接串、HTTP三层都要utf-8现象后台正常但 App 列表里中文显示成 ????或者一个接口返回的商品名称正常、另一个接口乱码。原因数据库表不是 utf8mb4或者 JDBC 连接串没指定 characterEncoding或服务端没有统一请求响应编码。三处只要有一处不一致中文就可能炸。解决建表语句统一用 CHARSETutf8mb4JDBC 连接串加 useUnicodetruecharacterEncodingutf-8Spring Boot 里在 application.yml 确认 server.servlet.encoding 的 force 为 true。Android 端 Retrofit 的 GsonConverterFactory 默认按 UTF-8 解析一般不用额外设置。排查顺序是先用数据库客户端看表里中文是否正常再用浏览器直接调接口看返回最后才看 App。一层一层定位不要上来就怀疑前端。5.5 布局嵌套太深与UI卡顿过度绘制与阴影滥用现象列表滚动不跟手、掉帧或者卡片四个角出现黑色块。原因CardView 外面又包了一层带背景的布局加上多个 View 互相叠加导致同一像素被绘制多次。阴影和半透明背景是过度绘制的主要来源。解决在开发者选项里打开“调试 GPU 过度绘制”保证页面大多数区域是蓝色不要出现大面积红色。布局上优先用 ConstraintLayout 拍平层级卡片内部用 LinearLayout 就够了不要在卡片外再套 FrameLayout。阴影只在需要区分的层级加首页数字卡片用 2dp列表 item 直接用 Divider 分割不要每一条都套阴影。Item 根布局背景设置纯色不要用半透明能显著减少过度绘制。这些操作做完录屏的流畅度会直观变好答辩现场也不会出现“点一下等半天”的尴尬。6. 演示级验证与答辩加分动作验收清单再走一遍课设做到能跑还不够还要做到演示不翻车。我的习惯是答辩前一天不写新功能只做三件事预置演示数据、预演断网降级、检查版本管理。这三个动作里都藏着血泪经验。演示数据预置很关键。用 schema.sql 建表之后顺手插一批商品和几笔流水让登录进去第一眼看到的是有数据的页面。不要现场手动录入那会浪费宝贵的答辩时间。预置数据要贴近真实商品名不要叫“商品A”“商品B”用“镀锌钢管”“水泥袋装”这种一眼就像行业系统印象分立刻不一样。断网降级也要提前想清楚。前后台分离的项目一旦断网App 会报网络异常。答辩现场 Wi-Fi 不稳是常态所以列表接口拿到数据后把最近一次成功响应的 JSON 存到本地文件断网时先展示旧数据并带一行“离线数据”的提示。这个细节不大但能在现场避免“点开页面白屏”的尴尬。答辩现场最加分的两个验证动作我建议提前练两遍。第一个在 App 里做一次入库切到后台的 MySQL 客户端执行 SELECT stock FROM goods WHERE sku_code...让老师看到库存字段真的变了。第二个打开后台控制台日志在 App 里点击刷新列表后台打出新的请求记录证明数据是通过 HTTP 从后台拿的、不是 App 里写死的。这两步足以把“前后台分离”从概念变成现场实证。最后讲一个让我长记性的习惯从写第一行代码起就配好 Git托管到 gitee 或 github每完成一个功能 commit 一次。我吃过一次亏整个项目改到一半UI 调坏了想回退只能靠 CtrlZ 慢慢找那个下午非常痛苦。配好 Git 之后每个节点都有后悔药课设文档里的版本迭代记录也能直接生成答辩老师问“这个系统迭代过几次”时你能拿 commit 记录说话比口头描述可信得多。希望这些从架构、业务、UI 到联调的经验能帮到你仓库管理系统课设按这条线走完平稳落地问题不大。本文还有配套的精品资源点击获取
