Android班费管理系统源码解析:质疑-确认闭环与Gradle构建实践
简介这套基于Java开发的安卓班费管理系统源码面向计算机专业学生与初级Android开发者适用于课程设计、毕业设计或实际工程项目参考。系统围绕班级班费管理场景实现了收支记录、明细查询、开支质疑与班委应答、全员可见的确认机制支持半数以上成员确认且质疑发起人确认后记录标记为有效有效提升班费使用的透明度和公信力。资源包共包含200个文件压缩后大小7.17MB其中以125个XML文件界面布局与资源配置、24个Java源文件核心业务逻辑为主另含WebP/PNG图片、Gradle构建脚本、Properties配置文件等项目结构清晰便于按模块阅读和二次开发。资源已有330人浏览学习。下载后可直接导入Android工程查看班费记录、质疑流程、状态标记等核心代码也能学到Android项目组织方式、数据存储与状态管理思路适合作为班费管理类应用的改造起点。1. 班费管理的透明化解法为何要一个带质疑机制的安卓系统很多班级的班费管理卡在同一个点账目记了没人看有人问了没人回。这个基于Java的安卓班费管理系统把班级记账场景做成了一套可运行的Android源码。它的核心不是流水账而是把“质疑—确认”机制写成了代码逻辑班委记录每一笔开支所有成员随时可查对某笔开支有疑问7天内可发起质疑班委必须回复质疑与应答全员可见只有超过半数成员确认、且所有质疑都被发起人认可后这笔记录才被标记为有效。适合接手班级事务的学生干部也适合想复现多角色交互、状态流转与权限控制的Android开发者。下面从工程结构一路拆到构建、踩坑和扩展尽量把每处设计都讲透。2. 196个文件的工程解剖从Gradle缓存到XML资源目录2.1 源码目录划分24个Java文件如何对应班费业务模块拿到压缩包先别急着丢进Android Studio先把目录结构认一遍。这个工程遵循Android标准结构24个Java源文件按职责拆进了五个包下面只列出与班费主流程强相关的类app/src/main/java/classfee/ ├── activity/ │ ├── LoginActivity.java // 登录与角色判断 │ ├── MainActivity.java // 班费总览列表 │ └── DetailActivity.java // 单笔开支详情、质疑入口 ├── model/ │ ├── ExpenseRecord.java // 开支记录实体 │ ├── Dispute.java // 质疑实体 │ ├── Vote.java // 确认投票实体 │ └── Member.java // 成员与班委角色 ├── adapter/ │ ├── ExpenseAdapter.java // 开支列表适配器 │ └── DisputeAdapter.java // 质疑会话适配器 ├── db/ │ ├── DBHelper.java // SQLite建库与升级 │ └── ExpenseDao.java // 支出与质疑的读写 └── util/ ├── PrefsManager.java // 本地会话与角色缓存 └── Validator.java // 质疑期、确认数校验逻辑说明activity层管页面跳转和生命周期model层只定义字段adapter层把数据绑定到列表db层封装SQLite读写util层放校验逻辑。24个Java文件按这个颗粒度分摊后每层职责都不重叠后期改权限逻辑只需要动util和activity不用碰db。参数说明包名这里用classfee命名实际克隆下来后包名可能不同导入时以工程里的applicationId为准。如果想给自己班级定制全局替换applicationId和包路径就行后面第4章会说替换位置。这个分层方式对班费场景特别合适。班费系统的核心诉求是操作可追溯、状态可回查如果所有逻辑都堆在MainActivity里一个页面写两三千行改一条质疑流程容易把别处带崩。拆开之后每次改动的影响面能被控制在一个包甚至一个类里。这种工程组织方式不新鲜但在这个项目里刚好压得住业务规模——24个Java文件五层结构既不臃肿也不缺层。2.2 Gradle构建链路3个Gradle文件与7个BIN缓存文件工程里带着3个Gradle文件分别是settings.gradle、根目录build.gradle、app/build.gradle三个文件的分工如下文件职责settings.gradle声明工程包含的模块这里只有一个app模块根build.gradle声明仓库源、全工程公共依赖版本、Android Gradle Plugin版本app/build.gradle声明compileSdk/minSdk/targetSdk、依赖库与打包参数还有7个BIN文件分散在gradle缓存与build目录下分别是executionHistory.bin、fileHashes.bin、outputFiles.bin、sha1-checksums.bin、resourceHashesCache.bin、md5-checksums.bin、last-build.bin。这些是Gradle增量构建的产物记录上一次构建时各个Task的输入输出哈希。Gradle靠它们判断哪些Task可以跳过、哪些必须重跑。executionHistory.bin存Task执行历史fileHashes.bin存文件内容哈希outputFiles.bin存产物路径sha1和md5两套哈希分别服务不同缓存阶段last-build.bin标记最近一次构建状态。它们不该被提交到版本库工程里的.gitignore已经把它们列进去了# Gradle .gradle/ build/ app/build/ local.properties # IDE .idea/ *.iml # OS .DS_Store逻辑说明前面四个BIN在.gradle/caches目录下后面三个在app/build目录下克隆下来后不需要手动清理Gradle首次构建时会重新生成。如果你拿到的压缩包里带了这些缓存文件构建失败时先删掉再重试有时候缓存错乱比源码出错更难查。参数说明.gitignore里的local.properties是关键这个文件记录了本机SDK路径属于个人环境配置一旦被提交换一台机器同步时会把别人的SDK路径带到你的环境里。如果Android Studio报“SDK location not found”优先检查这个文件是否正确。这里有一个打包时容易犯的错直接把整个工程目录压成压缩包连.gradle和build一起带上体积从几十MB膨胀到几百MB。共享资源时如果平台限制单文件大小先手动删除.gradle、build、.idea三个目录再压缩保留src、Gradle配置和gradlew脚本就够了。我一般会顺手把压缩包重新打开检查一遍确认没有缓存文件混进去再发布。2.3 XML资源体系与图片格式125个XML、WebP与PNG的搭配125个XML在Android工程里主要分三类布局文件、values资源、drawable形状文件。XML类别典型文件作用layoutactivity_main.xml、item_expense.xml、item_dispute.xml定义页面骨架和列表项结构valuesstrings.xml、colors.xml、styles.xml集中管理文案、颜色、样式drawablebg_button_primary.xml、selector_status.xml按钮背景、状态切换效果一个班费系统要一百多个XML并不夸张。每个列表项、每个按钮背景都是独立XML一个item_expense.xml里嵌套了支出名、金额、日期、状态四个控件拆开写便于复用。10个WebP图片和4个PNG图片分别承担图标与启动图WebP在同等画质下体积约为PNG的六到七成在低端手机上解码也更快。如果后续要换班级logo直接替换res/mipmap目录下的同名文件即可。5个IML文件是IntelliJ IDEA为每个模块生成的描述文件标记模块名与依赖关系。它不属于运行需要的文件删掉后Android Studio根据settings.gradle会自动重建。5个LOCK文件是Gradle Daemon锁文件出现在.gradle/caches目录下防止多个Gradle进程同时写同一份缓存同样不影响代码逻辑。这些文件很多人完全不碰我也是在替别人清理工程时才逐个认清楚——它们看着像垃圾丢了却可能引发奇怪的同步问题。3. 质疑-确认闭环的数据设计状态机、阈值与Java核心类3.1 数据模型与字段开支记录如何串联质疑与投票这个系统最核心的部分不是界面而是四张业务表之间的关联方式。支出记录是主对象质疑和投票都挂在它下面。表/实体关键字段说明memberid、role、student_no、namerole区分班委与普通成员expense_recordid、title、amount、category、payer_id、create_time、record_statusrecord_status标记是否有效disputeid、expense_id、questioner_id、content、reply_content、replier_id、statusstatus区分待回复、已回复、已确认voteid、expense_id、voter_id、agree、create_time一条记录一个成员投一票逻辑说明一个expense_id被查出来时列表页需要同时聚合出“质疑数、待回复数、已确认数、赞成票数、成员总数”才能展示透明化进度。通常做法是在ExpenseDao里写一个联查方法一次把主记录和统计值都取出来避免在Java层做N次独立查询。单独查成员总数时要排除已退出的成员这个边界容易漏。参数说明7天质疑窗口建议用expense_record的create_time加7天作为截止时间不另存end_time字段。这样调整窗口期只改一个常量不需要做数据库迁移。我在类似项目里都是用这个方案真到了要临时延长某笔记录的质疑期单独存一个extend_deadline字段做覆盖即可。3.2 状态机与判定阈值从待确认到标记有效的完整流转整个系统的规则可以压缩成三条第一质疑必须在7天内发起第二所有质疑都要有班委回复且被质疑发起人确认第三投票人数过半且赞成票过半记录才能标记为有效。把这三条翻译成状态判定方法// 状态判定示例判断一笔班费记录是否可以标记为有效 public class RecordStatusChecker { public static final long DISPUTE_WINDOW_MS 7L * 24 * 60 * 60 * 1000; // 7天质疑期 public static boolean canBeClosed(ExpenseRecord record, int totalMembers, ListDispute disputes, ListVote votes) { // 1. 质疑必须全部被回复且被发起人确认 for (Dispute d : disputes) { if (d.getStatus() ! Dispute.STATUS_CONFIRMED) { return false; } } // 2. 投票人数必须超过全体成员半数 if (votes.size() totalMembers / 2) { return false; } // 3. 赞成票必须超过投票人数的半数 int agreeCount 0; for (Vote v : votes) { if (v.isAgree()) agreeCount; } return agreeCount votes.size() / 2; } }逻辑说明代码按“质疑 → 人数 → 票数”三步依次校验每一步不满足就直接返回false。这样在列表页刷新时可以快速判断某条记录能否被更新为有效状态而不需要把完整逻辑散落到Activity各处。参数说明这里votes.size() totalMembers / 2用的是整除如果总人数是77/23实际需要4人投票才能过半。这个整除边界在UI上一定要提示否则班级40人时显示“已有19人投票”但按钮不可点用户会以为是Bug实际上是差1人才到半数。测试时可以把DISPUTE_WINDOW_MS临时改成5分钟验证质疑窗口过期逻辑发布前再改回来。3.3 Java核心类的职责切分谁负责记录、谁负责校验、谁负责展示流程可以简化为一个四步链路用户点击“确认该笔有效” - DetailActivity 收集当前expense_id - ExpenseDao.queryDisputes(expenseId) 得到未确认质疑列表 - Validator.checkDisputeReplied(disputes) 判断全部已回复 - Validator.checkMajority(totalMembers, votes) 判断半数确认 - ExpenseDao.updateRecordStatus(expenseId, closed) - 广播刷新列表这个“收集数据 → 校验 → 落库 → 刷新”的四步在Android里最直观出错时能通过Logcat定位到具体步骤。另一个可选方案是把状态判定写进SQL子查询直接在update语句里用count判断但SQL子查询可读性差出问题时很难看到中间数据。我一般建议先在Java层完成判定等业务跑稳了再考虑优化SQL。24个Java文件这个规模用Activity直接操作数据库也能跑通但如果你想扩展角色权限就得考虑把数据加载从Activity里挪出去。常见做法是给MainActivity配一个ViewModel数据加载交给它Activity只负责渲染和事件分发。这个项目源码没有引入重量级框架保持轻量分层是合理的——班费系统逻辑不复杂强行上MVP或MVVM反而增加学习成本。4. 从源码到可运行APKGradle环境、构建命令与参数调整4.1 环境准备JDK版本、SDK路径与wrapper版本对齐复现这个项目的第一步不是打开Android Studio而是把三样东西对齐JDK版本、Android SDK路径、Gradle wrapper版本。先看gradle/wrapper/gradle-wrapper.properties里的distributionUrl它决定Gradle发行版版本号然后根据AGP版本选择JDK。Gradle 6.x搭配JDK 8或11都能跑Android Gradle Plugin 4.x开始推荐JDK 11。SDK路径写到local.properties里Windows与Mac写法不同sdk.dirD\:\\Android\\Sdksdk.dir/Users/me/Library/Android/sdk逻辑说明local.properties包含本机路径不要提交到版本库。如果克隆下来后Android Studio报“SDK location not found”第一件事就是新建这个文件把SDK路径写对而不是去改项目源码。参数说明工程里那4个Properties文件里gradle.properties控制Gradle JVM参数与AndroidX开关比如org.gradle.jvmargs-Xmx2048m。如果你构建时报内存不足先改这里的堆大小不要急着调系统环境变量。提示同时打开多个Android Studio窗口时多个Gradle Daemon会抢占内存低配机器容易构建到一半卡死构建前先关掉不相关的工程窗口。4.2 命令行构建gradlew.bat assembleDebug与首次构建流程在工程根目录执行构建命令Windows用gradlew.batMac/Linux用./gradlew# 首次构建前可以清一次历史缓存 ./gradlew clean # 构建Debug安装包 ./gradlew assembleDebug # 产物路径 # app/build/outputs/apk/debug/app-debug.apk逻辑说明clean会删除build目录下所有增量产物第一次构建时并不强制但如果是从别人手里拿到的压缩包建议先clean一遍避免旧缓存和新源码错位。assembleDebug是Debug构建任务生成的app-debug.apk可以直接安装调试。参数说明首次构建会下载Gradle发行版和依赖库耗时五到十五分钟都很正常。如果shell报“gradlew.bat不是内部或外部命令”先确认当前目录确实是工程根目录再看JDK是否已加入PATH。构建完成后安装到设备adb install -r app/build/outputs/apk/debug/app-debug.apk-r表示覆盖安装并保留数据。测试质疑流程时更建议用真机模拟器时间跟随宿主机改动系统时间会影响7天质疑期的判定。4.3 参数调整minSdk、targetSdk、包名与界面文案工程跑起来后第一件该做的事是把包名和应用名改成自己班级的。打开app/build.gradle找到defaultConfigandroid { compileSdk 33 defaultConfig { applicationId com.example.classfee minSdk 21 targetSdk 33 versionCode 1 versionName 1.0 } }逻辑说明applicationId决定安装后的应用标识想换班号直接全局替换这个字符串。minSdk 21意味着Android 5.0及以上都能安装覆盖面足够广如果班里同学都在用近几年的手机可以抬到26往上省掉一部分兼容代码。参数说明compileSdk和targetSdk建议保持一致。targetSdk 30以上对存储权限更严格如果项目里用到外部存储读写测试时需要留意分区存储的适配。应用名和班级名集中放在values/strings.xml里resources string nameapp_name某班班费管家/string string nameclass_name示例班/string string namedispute_tip对本笔开支有疑问请在7天内提交质疑/string /resources逻辑说明文案写进strings.xml而不是硬编码在Java里改一处即全局生效后续做英文版时在res目录下新建values-en对应的strings.xml就行。XML里中文字符串默认就是UTF-8不需要额外转码。5. 避坑排查清单构建失败、界面异常与数据丢失的解法5.1 现象Gradle同步卡在依赖下载或构建长期不动第一次构建时Gradle需要下载自身发行版和第三方依赖。网络波动或下载源限速时同步进度条可能十几分钟都不往前走看起来像死掉了。原因gradle-wrapper.properties里的distributionUrl指向的下载源在当前网络环境下访问慢或者第三方仓库响应超时。解决通过init.gradle全局脚本把仓库地址替换为当前网络环境可用的镜像源// 放在用户目录的 .gradle 文件夹下命名为 init.gradle // 请将URL替换为你所在网络环境可用的Maven镜像地址 allprojects { repositories { maven { url https://your-mirror.example.com/repository/google/ } maven { url https://your-mirror.example.com/repository/jcenter/ } maven { url https://your-mirror.example.com/repository/public/ } } }参数说明init.gradle是全局初始化脚本放在用户目录的.gradle下后重启Android Studio即可生效。另一种快速验证方法是打开gradle-wrapper.properties把distributionUrl换成镜像里的同版本压缩包地址。这个坑在换电脑时最容易踩同一个工程在一台机器秒开另一台卡半小时差的就是镜像配置。5.2 现象assembleDebug报“Could not find method implementation()”App模块里用了implementation关键字引入依赖构建脚本直接报方法找不到。原因根build.gradle声明的Android Gradle Plugin版本低于3.0旧版AGP只认compile关键字不认识implementation。解决把根build.gradle里的AGP版本升到4.xbuildscript { dependencies { classpath com.android.tools.build:gradle:4.2.2 } }参数说明AGP 4.2.2对应Gradle 6.7.1以上升级后同步可能要重新下载插件。如果不想动AGP版本也可以把implementation改回compile但新版本SDK又可能提示compile已废弃。两害相权升AGP一个台阶更稳妥。报错信息里通常会带出当前AGP版本号先看清楚再动手。5.3 现象APK在低版本手机上闪退Logcat显示NoClassDefFoundErrorDebug包在Android 5.0的真机上运行启动即闪退Logcat里抛NoClassDefFoundError。原因minSdk设置高于真机系统版本或者某个依赖库引用了高版本API才有的类。Android上java.time包在API 26才有如果代码里用了LocalDateTimeminSdk低于26时直接崩。解决按需降低minSdk同时开启Java 8脱糖android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }参数说明如果代码里用了java.time类还要在defaultConfig里开启coreLibraryDesugaring设置否则低版本设备依旧崩。这个报错我折腾过一晚上属于典型的“版本暗坑”报错信息不直接指向你的代码而是指向某个系统类。5.4 现象数据库升级后原有班费记录全部消失App从1.0升级到1.1打开后发现历史班费流水和质疑记录全没了。原因DBHelper的onUpgrade里直接执行了DROP TABLE再CREATE TABLE旧数据被清空。解决升级表结构时保留旧数据用增量ALTER代替删表重建Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { // 2.0版本起给支出记录增加“备注”字段不清空旧数据 db.execSQL(ALTER TABLE expense_record ADD COLUMN remark TEXT DEFAULT ); } }逻辑说明班费系统一旦上线记录的是真金白银最忌讳升级即清库。正确做法是为每张表规划好版本号从旧版本到新版本之间只做增量变更。第一次发版时就把这段逻辑写好后续所有人升级都不会受影响。5.5 现象列表状态不刷新必须重启App才更新在详情页确认一笔开支后返回列表页状态还是旧值杀进程重开才正常。原因详情页更新数据库后没有通知列表页刷新列表页用onResume里的旧缓存直接渲染。解决在列表页的onResume里重新拉一次数据Override protected void onResume() { super.onResume(); loadExpenseList(); // 每次回到前台重新查询列表 }参数说明这个方法简单但足够稳。如果想做成局部更新需要记录上一页传入的expenseId返回时定位到对应Item做单条刷新。班费系统一次加载的数据量不大整页刷新也不会卡顿先用onResume方案性能不够再优化。6. 进阶扩展给班费系统加一个CSV期末账单导出6.1 CSV导出与中文乱码处理期末时班委最需要的是把一学期有效开支导出来给全班过目。在MainActivity的菜单里加一个“导出账单”入口点击后走这段逻辑// 导出已确认有效的班费记录为CSV public static File exportCsv(Context context, ListExpenseRecord records) throws IOException { File dir context.getExternalFilesDir(export); File file new File(dir, class-fee- System.currentTimeMillis() .csv); StringBuilder sb new StringBuilder(日期,项目,金额,经办人,状态\n); SimpleDateFormat sdf new SimpleDateFormat(yyyy-MM-dd, Locale.CHINA); for (ExpenseRecord r : records) { sb.append(sdf.format(new Date(r.getCreateTime()))) .append(,).append(r.getTitle()) .append(,).append(r.getAmount()) .append(,).append(r.getPayerName()) .append(,).append(r.getStatus()) .append(\n); } FileOutputStream fos new FileOutputStream(file); fos.write(sb.toString().getBytes(UTF-8)); fos.close(); return file; }逻辑说明导出文件放在应用私有外部目录getExternalFilesDir里不需要申请存储权限也符合Android 10及以上的分区存储要求。文件名带时间戳避免一次期末账单重复导出时互相覆盖。参数说明中文CSV在Excel里打开容易乱码原因是Excel默认按ANSI解析。我的做法是把文件头改成UTF-8带BOM在写文件前先写出三个字节的BOM标记。这个坑我踩过一次导出后发给别人一看全是乱码检查了半天才发现是编码格式问题。6.2 分享到班级群与FileProvider权限声明导出文件后通常还要分享到班级群。Android 7以上直接传File的Uri会抛FileUriExposedException需要借助FileProviderIntent share new Intent(Intent.ACTION_SEND); share.setType(text/csv); Uri uri FileProvider.getUriForFile(context, context.getPackageName() .fileprovider, file); share.putExtra(Intent.EXTRA_STREAM, uri); share.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); context.startActivity(Intent.createChooser(share, 导出班费账单));逻辑说明getUriForFile会把真实路径替换成授权的content:// URI分享时对方通过临时授权读取。需要在AndroidManifest.xml里声明FileProvider组件并在res/xml/file_paths.xml中把export目录配置为可共享路径。参数说明file_paths.xml里的path要写成导出目录的相对路径写错会报“Failed to find configured root”。我一般会把export目录单独列出来不给整个外部存储的权限这样即使分享链接被转发对方最多也只能读导出文件夹里的内容。从那以后我每次给班费系统加功能都强制走一遍“从构建到导出”的完整链路确认新代码没有破坏原流程再把APK交给班委试用。希望帮到你。本文还有配套的精品资源点击获取