简介:面向毕业设计场景的Android财务系统完整项目资源包,适合计算机相关专业学生与需要快速搭建财务类App的开发者。资源共1226个文件,压缩包仅10.86MB,以Java源码、XML布局、PNG图片、JAR依赖库等类型为主,包含完整的界面布局、业务逻辑、图标资源与第三方库配置,可直接导入Android Studio运行。系统覆盖登录验证、公告管理、账单管理、账单统计、个人中心、用户管理六大模块,支持管理员与注册用户不同角色的操作闭环,其中收入支出统计与密码修改等功能可满足毕业设计常见功能要求。全部代码与配置按Android工程标准目录组织,便于阅读、调试和二次扩展。目前已有167人学习下载,适合作为毕业设计参考或项目实战训练素材。
1. 为什么一个“财务系统”毕业设计要先想清楚身份认证
很多同学拿到这个题目,第一反应是先把账单增删改查写出来,结果做到一半发现登录、权限、公告、统计全缠在一起,改一处崩三处。这个基于 Android 的财务系统,表面上是六个功能模块,实际上是一条完整的数据链路:认证决定“你是谁”,用户管理决定“你能看到谁”,账单和统计决定“你看到什么”,公告和个人中心则是系统运转的辅助闭环。如果一上来就闷头写界面,后面对接后端接口时一定会反复返工。这篇文按“认证 → 用户 → 公告 → 账单 → 统计 → 设置”的顺序拆开讲,每部分都给出可落地的代码片段和参数说明,适合正在做 Android 课程设计、或者想补全一个完整业务闭环的开发者参考。
2. 登录模块与用户管理的权限基线
2.1 身份验证的常见实现方式
登录模块在这个项目里承担的是“门禁”角色,它不只要验证账号密码,还要为后续的公告管理、用户管理提供权限判断依据。我见过不少毕业设计把登录做成“本地比对用户名密码”,这种做法演示起来没问题,但换到真实场景就有隐患:客户端拿到的用户列表是完整的,攻击者反编译 APK 就能看到全部账号。常见做法是把登录校验放到服务端,Android 端只负责提交账号密码并接收 token。
客户端代码可以这样写:
// LoginActivity.java private void doLogin(String account, String password) { // 先做本地非空校验,避免无谓的网络请求 if (TextUtils.isEmpty(account) || TextUtils.isEmpty(password)) { showToast("账号和密码不能为空"); return; } // 构建请求体,这里用 JSON 格式传递 JSONObject body = new JSONObject(); try { body.put("account", account.trim()); body.put("password", MD5Util.md5(password)); // 密码先做 MD5 再传输 } catch (JSONException e) { e.printStackTrace(); return; } // 发起异步请求,回调中处理登录结果 HttpHelper.post("/api/login", body.toString(), new HttpCallback() { @Override public void onSuccess(String response) { // 解析 token 并保存到 SharedPreferences String token = JSONParser.parseToken(response); SharedPreferencesUtil.putString("token", token); navigateToMain(); } @Override public void onFailure(int code, String msg) { showToast("登录失败: " + msg); } }); }这段代码有两个关键参数需要说明:MD5Util.md5(password)是对密码做摘要处理,防止明文出现在请求日志里,但要注意 MD5 本身不够安全,生产环境建议换成 BCrypt;HttpHelper.post是封装好的网络请求工具,实际项目里可以用 OkHttp 或 Retrofit 替代。每次登录成功后保存的 token,是后续所有业务接口的通行凭证。
2.2 用户管理模块的权限控制逻辑
用户管理模块在这个系统里是由管理员操作的,它的核心不是界面能做多少按钮,而是服务端怎么保证“只有管理员能调用这些接口”。常见做法是在服务端拦截器里校验 token 关联的 role 字段,Android 端根据登录时返回的角色值来决定是否显示“用户管理”入口。
// MainActivity.java // 登录成功后获取用户角色:1 表示管理员,2 表示普通用户 int role = SharedPreferencesUtil.getInt("role", 2); if (role == 1) { // 只有管理员能看到用户管理入口 findViewById(R.id.btn_user_manage).setVisibility(View.VISIBLE); findViewById(R.id.btn_announcement_manage).setVisibility(View.VISIBLE); } else { findViewById(R.id.btn_user_manage).setVisibility(View.GONE); findViewById(R.id.btn_announcement_manage).setVisibility(View.GONE); }用户管理界面通常是一个 RecyclerView 列表,配合搜索框和弹窗表单。添加或编辑用户时,需要提交的字段包括账号、密码、电话、用户类型、姓名、性别、学历。这里的“用户类型”实际上就是权限分组的依据,我建议把用户类型做成下拉选择而不是自由输入,避免脏数据影响后续判定。
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| account | String | 唯一,必填 | 登录账号 |
| password | String | 必填 | 建议 MD5 后入库 |
| phone | String | 选填 | 联系方式 |
| userType | int | 必填,1或2 | 1管理员,2普通用户 |
| name | String | 必填 | 真实姓名 |
| gender | String | 选填 | 男/女 |
| education | String | 选填 | 学历信息 |
修改用户信息时注意一个细节:如果密码字段留空,说明不修改密码,后端要加这个判断,否则每次编辑用户都会被强制重置密码。
提示:删除用户前一定要做二次确认弹窗,并且判断当前操作者不能删除自己,这是管理类系统最容易踩的坑。
3. 公告管理模块的 CRUD 与列表刷新策略
3.1 公告列表的加载与分页
公告管理模块的操作对象是“标题、内容、发布时间”这三类信息,功能是增删改查。从 Android 端角度看,它比用户管理多了一个分页问题:公告数量会随着时间增长,如果一次性全量加载,列表会越拉越长,体验变差。
常见的分页方案有两种:一种是服务端做page和pageSize参数控制,另一种是客户端拿到全量数据后自行截断。前者适合真实项目,后者适合 demo。这里以前者为例:
// AnnouncementListActivity.java private void loadAnnouncements(int page, int pageSize) { String url = "/api/announcements?page=" + page + "&pageSize=" + pageSize; HttpHelper.get(url, new HttpCallback() { @Override public void onSuccess(String response) { List<Announcement> list = JSONParser.parseAnnouncementList(response); if (page == 1) { adapter.setData(list); } else { adapter.addData(list); } } }); }这里page从 1 开始计数,pageSize一般设 10 或 20。下拉刷新时重新加载第一页,上拉加载更多时页码加一。要注意的是,发布公告和编辑公告后,应该回到列表页并刷新第一页数据,而不是直接在本地修改列表里的一项,否则可能出现服务端数据与本地显示不一致的情况。
3.2 公告的新增与编辑共用表单
公告的新增和编辑界面通常可以共用一个 Activity,通过 Intent 传递一个announcementId来判断是“新增”还是“编辑”。这样做的好处是减少代码重复,但要注意表单回填时的时机:
// AnnouncementEditActivity.java // 判断是新增还是编辑 long announcementId = getIntent().getLongExtra("announcement_id", -1); if (announcementId != -1) { // 编辑模式,先根据 id 拉取详情并回填表单 loadAnnouncementDetail(announcementId); } else { // 新增模式,发布时间默认取当前时间 tvPublishTime.setText(DateUtil.getCurrentTime()); }发布时间字段在这个设计里有个容易忽略的点:新增时是服务端自动生成,还是客户端提交?如果是客户端提交,要考虑用户修改系统时间导致的数据偏差,我建议服务端在接收到新增请求时以服务器当前时间覆盖客户端提交的时间字段,保证时间线一致。
删除公告时,执行删除请求后刷新列表。删除接口一般要传announcementId,不要在列表项上只传 position,因为列表经过筛选排序后,position 对应数据不一定准确。
4. 账单管理模块的字段设计与增删改查
4.1 账单类型的选择与存储
账单管理模块的字段比公告多:类型、用途、金额、添加日期、备注。这里的“类型”在这个设计中是“收入”和“支出”两个大类下的细分,比如餐饮、交通、工资、奖金等。Android 端用一个表结构或本地数据库来承载这些数据都可以,如果项目是联网的,通常把账单数据存储到服务端,客户端展示。
比较推荐的实现是服务端存一张 bill 表,核心字段包括bill_id、user_id、bill_type、category、amount、bill_date、remark。其中user_id必须取自 token 解析出的当前登录用户,不能由客户端自由上传,否则用户可以随意伪造他人账单。
CREATE TABLE bill ( bill_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, bill_type INTEGER NOT NULL DEFAULT 1, -- 1收入 2支出 category TEXT NOT NULL, amount REAL NOT NULL, bill_date TEXT NOT NULL, remark TEXT, create_time TEXT DEFAULT (datetime('now')) );bill_type用 1 和 2 代替中文“收入/支出”,是为了减少存储空间和避免编码问题,展示层再映射为汉字。
4.2 账单列表的搜索与筛选
账单列表页一般需要一个搜索框,同时支持按日期范围筛选。常见的实现是客户端把关键字和日期范围作为查询参数传给服务端:
// BillListActivity.java String keyword = etSearch.getText().toString().trim(); String startDate = tvStartDate.getText().toString(); String endDate = tvEndDate.getText().toString(); String url = "/api/bills?keyword=" + URLEncoder.encode(keyword) + "&startDate=" + startDate + "&endDate=" + endDate; HttpHelper.get(url, callback);keyword可以模糊匹配用途、备注、类型名称;startDate和endDate默认给一个较大的跨度,比如今年第一天到今天。这里要注意字符串格式统一,建议全链路使用yyyy-MM-dd格式,避免因日期格式不一致查不到数据。
账单编辑和删除的细节与公告模块类似,不再重复。需要特别提醒的是金额字段的处理:服务端不要用float存钱,使用DECIMAL(10,2)或客户端用BigDecimal,否则多次累加会出现精度丢失。Android 端展示金额时也要避免直接fload amount = Float.parseFloat(...)后做运算。
4.3 bill 类型与统计的联动
账单类型和统计模块是联动的。统计页要按类型汇总金额,客户端展示时需要按bill_type分组求和。如果账单存的是“餐饮”“交通”这类细分分类,那么统计页要能同时支持“按大类汇总”和“按细分分类汇总”,这需要在服务端写一个GROUP BY查询:
SELECT bill_type, SUM(amount) AS total_amount FROM bill WHERE user_id = ? AND bill_date BETWEEN ? AND ? GROUP BY bill_type;这个查询返回的结果就是“我的收入总金额”和“我的支出总金额”的数据来源。在此基础上再增加category字段的分组,就能得到每个细分分类的占比。
5. 账单统计模块的图表展示与明细钻取
5.1 收支统计的聚合接口
账单统计管理模块,描述中提到由“我的收入、我的支出、收支统计”组成,这里的核心动作是“聚合”和“下发”。客户端要拿到“按日期/按类型汇总”的数据,需要一个专门的统计接口。
// BillStatisticsActivity.java private void loadStatistics(String startDate, String endDate) { String url = "/api/bills/statistics?startDate=" + startDate + "&endDate=" + endDate; HttpHelper.get(url, new HttpCallback() { @Override public void onSuccess(String response) { // 返回数据结构: { "totalIncome": 1234.5, "totalExpense": 567.8, // "incomeDetail": [...], "expenseDetail": [...] } StatisticsData data = JSONParser.parseStatistics(response); tvIncome.setText("¥" + data.totalIncome); tvExpense.setText("¥" + data.totalExpense); // 初始化图表控件,传入聚合数据 initPieChart(data.incomeDetail, data.expenseDetail); } }); }统计接口返回的数据里,incomeDetail和expenseDetail是分类汇总数组,通常包含每个分类的名称和金额。这样做的好处是一次请求拿到全部需要的数据,避免客户端多次循环调用普通账单列表接口再自行聚合。
5.2 饼图与柱状图的适配
Android 端实现图表,最常用的是 MPAndroidChart 这个开源库。在build.gradle中依赖:
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'初始化饼图的方式:
private void initPieChart(List<CategoryAmount> incomeData) { PieChart pieChart = findViewById(R.id.pie_chart); ArrayList<PieEntry> entries = new ArrayList<>(); for (CategoryAmount item : incomeData) { entries.add(new PieEntry(item.amount.floatValue(), item.categoryName)); } PieDataSet dataSet = new PieDataSet(entries, "收入构成"); dataSet.setColors(ColorTemplate.COLORFUL_COLORS); PieData data = new PieData(dataSet); data.setValueFormatter(new PercentFormatter(pieChart)); pieChart.setData(data); pieChart.setUsePercentValues(true); pieChart.setEntryLabelTextSize(11f); pieChart.invalidate(); }PieEntry的第一个参数是数值,第二个参数是显示标签;setUsePercentValues(true)会让饼图显示百分比而不是具体金额,适合看各分类占比。如果数据里有金额为 0 的分类,最后会显示一个零占比,建议在设置数据前过滤掉。
5.3 点击扇形区域钻取明细
统计界面还有一个常见的交互:点击某个扇形区域,跳到对应分类的账单明细列表。MPAndroidChart 提供了OnChartValueSelectedListener:
pieChart.setOnChartValueSelectedListener(new OnChartValueSelectedListener() { @Override public void onValueSelected(Entry e, Highlight h) { // 这里的 label 就是分类名 String categoryName = ((PieEntry) e).getLabel(); Intent intent = new Intent(StatisticsActivity.this, BillListActivity.class); intent.putExtra("category", categoryName); intent.putExtra("billType", currentBillType); startActivity(intent); } @Override public void onNothingSelected() { // 不做处理 } });跳转到 BillListActivity 后,列表页读取category参数,加在查询条件上,就能展示该分类下的所有账单。这里要注意拼接 URL 时分类名要做 URL 编码,否则中文参数会被截断或乱码。
6. 个人中心密码修改与列表性能优化细节
6.1 修改密码的校验逻辑
个人中心管理模块按要求是修改密码:输入原密码、新密码、确认密码。这个功能的实现并不复杂,真正容易出错的是“原密码校验”的位置。如果本地保存了密码的 MD5,可以在本地比对原密码;如果没有保存,就必须把原密码透传给服务端校验。
// ChangePasswordActivity.java private void doChangePassword(String oldPwd, String newPwd, String confirmPwd) { if (!newPwd.equals(confirmPwd)) { showToast("两次输入的新密码不一致"); return; } if (newPwd.length() < 6 || newPwd.length() > 20) { showToast("密码长度需在6-20位之间"); return; } JSONObject body = new JSONObject(); try { body.put("oldPassword", MD5Util.md5(oldPwd)); body.put("newPassword", MD5Util.md5(newPwd)); } catch (JSONException e) { e.printStackTrace(); return; } // 提交给服务端,由服务端判断原密码是否正确 HttpHelper.post("/api/user/changePassword", body.toString(), new HttpCallback() { @Override public void onSuccess(String response) { showToast("密码修改成功,请重新登录"); SharedPreferencesUtil.clear(); navigateToLogin(); } @Override public void onFailure(int code, String msg) { showToast("修改失败: " + msg); } }); }有个细节值得注意:每次输错新密码,旧密码页面不要清空,只清空两个新密码输入框即可,这样用户体验更好。修改成功后,清理本地 token 并强制回到登录页,避免旧 token 继续使用。
6.2 RecyclerView 列表的性能优化
整个项目里,用户管理、公告管理、账单管理都要用列表展示数据,RecyclerView 的性能直接影响体验。几个实用的优化点:
// BillListAdapter.java @Override public BillViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { // 1. 避免每次都重新 inflate 布局 View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_bill, parent, false); return new BillViewHolder(view); } @Override public void onBindViewHolder(BillViewHolder holder, int position) { BillItem item = getItem(position); // 2. 使用 setText 而不是每次创建新 String holder.tvAmount.setText("¥" + item.amount); // 3. 如果列表项很多,开启 setHasFixedSize }如果你不确定RecyclerView的布局是否固定,可以mRecyclerView.setHasFixedSize(true),这样当列表项内容变化时不会触发重新布局。配合onCreateViewHolder的复用机制,滚动卡顿的现象会明显改善。
6.3 发布前要检查的三个边界
不管这个项目是用于答辩还是自己学习,发布前建议对照下面几个边界条件过一遍:
第一,所有网络请求都要设置超时时间。OkHttp 默认连接超时是 10 秒,读超时也是 10 秒,如果你的服务端响应慢,客户端会出现 ANR 或长时间白屏。在 OkHttpClient 构建时设置connectTimeout(10, TimeUnit.SECONDS)和readTimeout(10, TimeUnit.SECONDS)是基本要求。
OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build();第二,AndroidManifest.xml中如果 targetSdkVersion 是 28 及以上,默认禁止明文 HTTP 访问,需要在res/xml/network_security_config.xml里配置允许特定网段或域名使用明文传输。很多同学在真机调试时遇到 “CLEARTEXT communication not permitted” 就是卡在这里。
<!-- res/xml/network_security_config.xml --> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="true">192.168.1.100</domain> </domain-config> </network-security-config>第三,退出登录时要调用服务端的注销接口,让 token 失效,而不仅仅是清空本地数据。否则账号已经在别处修改密码,旧 token 如果还在有效期,仍然可以访问部分接口,这样权限控制就是失效的。
本文还有配套的精品资源,点击获取