Android Studio个人记账APP源码详解:SQLite、列表与统计改造
发布时间:2026/9/29 1:57:30来源:尧图网络
简介一款基于Android Studio开发的个人记账工具APP源码面向正在学习安卓基础组件、界面搭建与本地数据库操作的开发者也适合用作毕业设计或课程项目的参考雏形。项目围绕日常收支记录展开支持录入当天收支自动汇总当天及当月总金额可随时查看历史账单并对每月收支进行柱状图与百分比分析还提供按关键字搜索特定记录的功能界面简约流畅基本覆盖个人记账核心需求。压缩包共164个文件以77个PNG图片素材、38个XML布局与样式文件、35个Java业务逻辑代码为主另附Gradle工程配置与数据库管理辅助文件整体仅346KB目录结构清晰目前已有2779人学习下载。源码集中演示了布局绘制与常用控件属性、Activity跳转传值、Fragment切换视图、自定义对话框及软键盘、列表和网格适配器、系统数据库增删改查、drawable样式定义并结合MPAndroidChart绘制统计图表技术点从界面到存储链路完整适合逐模块拆解也可在源码基础上扩展预算管理、分类统计等新功能。1. 从一套能跑的Android Studio记账源码里你能拿到什么在Android Studio里打开一份别人写好的个人记账APP源码是不少开发者第一次接触到完整Android工程的方式。这套标题里的“基于Android Studio开发的个记账工具APP源码Android 个人记账APP源码”本质上就是一个可以直接编译运行的个人记账工程功能通常围绕“记一笔支出”“按日期看流水”“按月看统计”展开。它解决的不是“记账”本身而是让你不用从零搭建项目结构直接看到Activity跳转、SQLite数据库、列表展示和简单图表在一套代码里怎么配合。适合课程设计、新手练手以及想快速改一个自用记账工具的人。这篇就按“先拆工程结构、再跑核心功能、最后讲坑”的方式把一个记账APP源码从导入到改造成型讲透。2. 拆工程骨架再动手改Gradle配置、项目目录与SQLite选型2.1 拿到源码第一件事核对Gradle版本和SDK路径很多人拿到源码后直接点Run结果Gradle sync就失败。原因是Android Studio版本、Gradle插件版本和compileSdkVersion对不上。大多数个人记账APP源码是老项目用的大概率是Groovy DSL的build.gradle而不是新版的Kotlin DSL。你在Android Studio里打开时它会提示用本地Gradle版本还是wrapper版本优先选wrapper。你需要检查的只有三个文件。项目根目录的build.gradle里声明插件版本app/build.gradle里声明compileSdk和依赖gradle-wrapper.properties里声明分发版本。如果sync报错说Gradle版本过低不是去官网下最新的而是把gradle-wrapper.properties里的distributionUrl改成和Android Studio匹配的版本。常见做法是让wrapper的版本和Android Studio菜单里显示的版本一致android studio会自动提示你升级或降级。// gradle-wrapper.properties distributionBaseGRADLE_USER_HOME distributionPathwrapper/dists distributionUrlhttps\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBaseGRADLE_USER_HOME zipStorePathwrapper/dists这个文件的修改决定了Gradle本身用哪个版本。Android Studio 2023年以后的大版本对应Gradle 8.x的比较多。如果你的机器上之前跑过别的项目Android Studio会优先复用本地Gradle反而忽略wrapper这时候要靠File - Settings - Build Tools - Gradle里的配置项来指定Gradle JDK和Gradle版本不能只改文件。// app/build.gradle android { namespace com.example.budgetapp compileSdk 34 defaultConfig { applicationId com.example.budgetapp minSdk 24 targetSdk 34 versionCode 1 versionName 1.0 } }namespace是Android Studio新版本要求的包名声明老源码里没有这行需要手动补。applicationId是应用唯一标识安装到手机上就是它。minSdk 24意味着Android 7.0以上都能跑如果源码里用了更高版本API而minSdk低于它编译期就会直接报错。targetSdk 34会影响系统行为适配比如Android 13以后的通知权限和Android 11以后的存储权限在这个字段上改动最明显。2.2 典型的个人记账APP工程分几层打开一个记账源码目录一般分成三块。第一块是activity和fragment负责页面跳转和生命周期第二块是adapter和viewholder负责把数据变成列表项第三块是dbhelper和model负责数据库操作和数据结构定义有的项目还会拆出util包放日期格式化和金额转换的工具类。个人记账APP不像商业项目拆那么细这三层已经足够跑通。app/src/main/java/com/example/budgetapp/ ├── activity/ │ ├── MainActivity.java │ ├── AddBillActivity.java │ └── StatisticActivity.java ├── adapter/ │ └── BillListAdapter.java ├── db/ │ ├── BillDbHelper.java │ └── BillModel.java ├── util/ │ ├── DateUtil.java │ └── MoneyUtil.java └── view/ └── PieChartView.javaMainActivity负责加载首页流水列表和底部导航或切换按钮AddBillActivity负责收入支出的录入StatisticActivity承载图表展示。BillDbHelper封装了SQLiteOpenHelper的增删改查。BillModel是一个纯数据类字段对应数据库表的列。PieChartView通常是自定义View在onDraw里用Canvas画饼图或环形图。你在改代码时要清楚一个边界activity里不应该直接写SQL语句。常见做法是在MainActivity里先new一个BillDbHelper然后调用queryAllBills()返回一个List 再扔给BillListAdapter。如果源码结构不是这样而是把SQLiteManager写成了单例直接静态访问也不影响运行只是测试起来费劲。2.3 为什么这些源码都选SQLite而不是Room或SharedPreferences个人记账APP源码里SQLite出现的频率非常高原因很实际SQLiteOpenHelper是Android SDK自带的不需要额外导入依赖对一个几百行的数据库操作来说完全够用。Room虽然开发体验更好但需要引入注解处理器和三层抽象源码体积变大很多课程设计和练手项目不会主动引入。SharedPreferences只能存简单的键值对遇到“查某个月所有支出总和”这种聚合查询就要把数据全部读出来再遍历数据一多就明显卡顿。SQLite在这个场景下的合理边界是数据量在十万条以内、单机使用、不需要远程同步。个人记账APP正好落在边界内。你用一个SQLiteOpenHelper的子类在onCreate里执行建表语句在onUpgrade里处理版本升级就可以覆盖全部需求。如果你拿到手的源码用了SQLite先别急着换Room除非你后续要做数据迁移或者多表关联查询否则属于给自己加工作量。public class BillDbHelper extends SQLiteOpenHelper { private static final String DB_NAME budget.db; private static final int DB_VERSION 1; public BillDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS bill ( id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, amount REAL NOT NULL, category TEXT NOT NULL, date TEXT NOT NULL, note TEXT, create_time TEXT DEFAULT CURRENT_TIMESTAMP)); } }DB_NAME是数据库文件名DB_VERSION是数据库版本号。升级表结构时只把版本号加一然后在onUpgrade里写ALTER语句这个版本号不增的话手机上不会执行onUpgrade改了建表语句也没用。type用0和1区分支出和收入比存“expense”这种字符串做聚合运算更高效。amount用REAL存金额注意浮点数在累加时会有精度问题后面统计章节会专门处理。这里要插一个新手高频翻车点很多人以为改了onCreate里的建表语句重新运行就能生效但APP安装过一次之后数据库文件已经生成SQLiteOpenHelper不会再执行onCreate。要让新字段生效要么卸载APP重装要么把DB_VERSION加一并在onUpgrade里写ALTER TABLE。这对记账源码来说尤其重要因为你的账本数据不像测试工程可以随手丢。3. 把“记一笔”到“本月花了多少”跑通SQLite增删改查、RecyclerView流水列表与图表统计3.1 记账核心表设计预算金额、日期格式、支出类型不能乱第2章的建表语句能跑但真正到统计环节你会被三个细节卡住。第一是日期格式第二是金额精度第三是分类字段的取值。个人记账APP源码里常见的日期存储格式是“yyyy-MM-dd”查询某个月的数据用 LIKE 2025-06%如果有人在代码里写入“2025/6/1”那这个月流水就永远查不出来。金额建议存成分单位的长整型比如“12.34元”存成1234用int或者long别用float。现在很多记账源码为了省事用REAL一旦涉及“本月总支出”累加0.10.2不等于0.3的经典问题就会浮出来。我的口径是入库前统一用BigDecimal换算成分出库显示时再除以100。稍微麻烦一点但统计报表不会给你颜色看。实现时在BillModel里加一个amountInCents字段SQLite列类型用INTEGERAddBillActivity里输入金额后用字符串截取而不是浮点运算避免用户输入12.34时内部变成12.3399999。public class MoneyUtil { public static long formatToCents(String input) { String clean input.trim(); if (!clean.matches(\\d(\\.\\d{1,2})?)) { throw new IllegalArgumentException(金额格式不对); } String[] parts clean.split(\\.); long yuan Long.parseLong(parts[0]); long cents 0; if (parts.length 1) { String dec parts[1]; if (dec.length() 1) { cents Long.parseLong(dec) * 10; } else { cents Long.parseLong(dec); } } return yuan * 100 cents; } }MoneyUtil把用户输入的“12.34”转成1234分。正则只允许整数位加最多两位小数等于在入口挡住不合法的输入。注意这里有坑如果用户输入“12.”这种以小数点结尾的字符串split后会得到两部分但dec是空字符串Long.parseLong会抛异常。所以要在parts.length 1之后先判断dec是否为空或者干脆在正则里加上不允许以点结尾的限制。很多人抄了这段代码但没处理空字符串测试时输入12.就闪退。3.2 首页流水列表RecyclerView 卡片样式 下拉刷新记账APP的首页一般就是一张流水列表。数据源来自BillDbHelper的queryAllBills()查询结果是一个List 然后传给RecyclerView.Adapter。这里要特别注意RecyclerView的item布局和Adapter的ViewHolder绑定逻辑。很多源码用ListView老项目为了省代码用ListView也不奇怪但你需要知道RecyclerView是现在的主流方案新写的代码建议按RecyclerView来。public class BillListAdapter extends RecyclerView.AdapterBillListAdapter.BillViewHolder { private final ListBillModel bills; public BillListAdapter(ListBillModel bills) { this.bills bills; } Override public BillViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 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) { BillModel bill bills.get(position); holder.tvCategory.setText(bill.getCategory()); holder.tvAmount.setText(bill.getAmountInYuan()); holder.tvDate.setText(bill.getDate()); if (bill.getType() 0) { holder.tvAmount.setTextColor(Color.parseColor(#E64A19)); } else { holder.tvAmount.setTextColor(Color.parseColor(#43A047)); } } }onCreateViewHolder负责创建条目视图onBindViewHolder负责把数据填充到视图上。这里有个型初学者会踩的坑bills列表在数据变化后必须调用adapter.notifyDataSetChanged()否则界面不会刷新。很多老源码在onResume里重新查一次数据库再调用notifyDataSetChanged这样从新增页面返回首页时数据才会更新。RecyclerView的item布局里支出金额和收入金额用不同颜色区分是记账APP最浅显但也最有用的视觉反馈。RecyclerView本身不解决“下拉刷新”需要配合SwipeRefreshLayout使用。旧源码里如果只有ListView你可以在外层套SwipeRefreshLayout在onRefresh里重新查询数据库再停止刷新动画。androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:idid/refresh_layout android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/recycler_view android:layout_widthmatch_parent android:layout_heightmatch_parent android:paddingBottom56dp android:clipToPaddingfalse / /androidx.swiperefreshlayout.widget.SwipeRefreshLayoutRecyclerView放在SwipeRefreshLayout内部是标准做法。paddingBottom加56dp是为了防止最后一个条目被底部导航栏遮挡clipToPadding设成false让滚动条可以滚到padding区域这样列表滚到底部时最有一条也能完整显示。这个细节那些拿iOS风格磨出来的记账APP一样适用Android端不用它就能被评论区问“最后一个条目怎么被挡住了”。3.3 统计视图Canvas画折线图进度条显示当月预算统计功能是记账APP源码的价值点也是最多人卡住的地方。大多数个人记账APP不会引入MPAndroidChart这类第三方库而是自己写一个View用onDraw的Canvas把上个月的支出和这个月每天支出画成折线。自己画的好处是不用学第三方库的API坏处是一切细节都要自己调。public class MonthLineView extends View { private final Paint linePaint new Paint(); private final Paint dotPaint new Paint(); private ListFloat values; public MonthLineView(Context context) { super(context); linePaint.setColor(Color.parseColor(#3F51B5)); linePaint.setStrokeWidth(4f); linePaint.setStyle(Paint.Style.STROKE); dotPaint.setColor(Color.parseColor(#E64A19)); dotPaint.setStyle(Paint.Style.FILL); } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (values null || values.size() 0) return; float maxValue Collections.max(values); float minValue Collections.min(values); float range maxValue - minValue; float stepX getWidth() / (values.size() - 1); Path path new Path(); for (int i 0; i values.size(); i) { float x i * stepX; float y getHeight() - 20f - (values.get(i) - minValue) / range * (getHeight() - 40f); if (i 0) { path.moveTo(x, y); } else { path.lineTo(x, y); } canvas.drawCircle(x, y, 4f, dotPaint); } canvas.drawPath(path, linePaint); } }这段代码的核心是把“日期”和“金额”映射到View的宽度和高度上。x坐标按天均分y坐标通过min-max归一化到View高度范围内避免某天金额特别大时整条线冲出屏幕。注意一个边界条件values.size()为1时getWidth()除以0会抛ArithmeticException所以要在传入数据时判断至少要有两个点。很多自定义统计View在只有一条数据时翻了车因为大家测试时随手录了十几条新手拿到真机只录了一条直接崩。统计页里还有个常用元素当月预算进度条。用ProgressBar的setMax和setProgress比如本月预算5000元已经花了3724元progress (int)(spent / budget * 100)。它的坑在预算为0时setMax(0)会抛异常需要先判断budget 0。进度条颜色改成超过80%变橙超过100%变红算是给用户一个直观反馈。progressBar.setMax(100); int percent (int) (spentCents * 100L / budgetCents); progressBar.setProgress(percent, true); if (percent 100) { progressBar.setProgressTintList(ColorStateList.valueOf(Color.RED)); }setMax放在setProgress之前。percent计算时先把spentCents转成long再乘以100防止int溢出。setProgress的第二个参数true表示带动画但如果你在onResume里刚设置完Activity还没完全显示动画有时会不执行这是Android显示时机的问题不影响最终效果。当进度条颜色变化后用setProgressTintList要注意这是API 21之后才有的方法minSdk低于21的项目直接编译错误。4. 个人记账APP源码最常见的5个坑启动闪退、列表空白、统计不对的排查顺序4.1 启动闪退AndroidManifest.xml里的Activity没有注册现象Android Studio点Run后编译成功APP安装到模拟器上打开瞬间闪退回桌面Logcat里能看到ActivityNotFoundException。原因新写的Activity没有在AndroidManifest.xml里声明。很多源码里为了省事把AddBillActivity、StatisticActivity这些页面写好了但没注册。解决在 节点里补上完整的Activity声明同时确认主入口Activity有MAIN和LAUNCHER的intent-filter。activity android:name.activity.AddBillActivity / activity android:name.activity.StatisticActivity / activity android:name.activity.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activityAndroid 12之后如果你给Activity设置了exported属性并且没有配intent-filter编译时会报警告但能跑如果要让别的应用也能唤起需要在带intent-filter的Activity上显式声明android:exportedtrue。个人记账APP只有自己用exportedfalse就够了。遇到闪退先看Logcat里有没有ActivityNotFoundException这是最快的定位方式。4.2 列表空白或高度只有一条RecyclerView嵌套ScrollView的measure问题现象首页列表能出数据但只在顶部显示一条下面全部空白或者整个页面无法滚动。原因外层套了ScrollViewRecyclerView高度被measure成wrap_content而RecyclerView的测量规则在嵌套滚动容器内会失效。解决不要把RecyclerView放在ScrollView里。如果首页结构是顶部一个卡片、下面整个列表用CoordinatorLayout或LinearLayout把卡片和列表正常排列RecyclerView高度设为match_parent或固定高度。Android中协调布局加Banner结构的页面在这个坑上非常典型顶部是一个Banner轮播下面是一个RecyclerView列表新手习惯性在外部套一个ScrollView让整个页面滚动结果列表就只剩一个item的高度。正确做法是让Banner作为RecyclerView的header item放在Adapter里或者用NestedScrollView配合RecyclerView并且调用recyclerView.setNestedScrollingEnabled(false)。这条踩坑在任何一个带列表的Android项目里都会出现记账APP隔离不了。4.3 统计金额对不上日期或金额格式混用了现象月度统计页面显示的总支出比流水列表里加起来的少很多或者某个月的统计恒为0。原因数据库里存的日期格式不统一有的记录是yyyy-MM-dd有的是yyyy/M/d统计时用LIKE 2025-06%匹配不到带斜杠的数据。另一个常见原因是金额用float累加多笔0.1元的支出加出了0.30000000000000004。解决写一个迁移方法在APP启动时扫描一遍bill表把所有日期统一成yyyy-MM-dd金额统一乘以100转成整数。一劳永逸的做法是第3章的按分存储而不是在统计时反复做字符串解析和浮点运算。这类问题最气人的地方在于它不会让你崩溃只会让数据看起来“不太对”。DB Browser for SQLite打开你的budget.db直接看date列的原始值就能确认是不是格式混用。如果数据量小直接在SQLite里update字段如果数据量上来了务必要写迁移代码而不是手动改库因为用户手机上的数据你是碰不到的只能靠APP自己修。4.4 新版系统上导出备份失败分区存储改变了文件访问现象源码里用Environment.getExternalStorageDirectory()直接写一个CSV文件到/sdcardDownload/在Android 10之前的模拟器上正常Android 11及以上就抛FileNotFoundException或IOException。原因从Android 10开始强制分区存储普通应用不能再随意访问公共目录的绝对路径。解决走MediaStore API导出CSV或者用SAF让用户自己选择保存目录。Intent intent new Intent(Intent.ACTION_CREATE_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(text/csv); intent.putExtra(Intent.EXTRA_TITLE, backup.csv); startActivityForResult(intent, REQUEST_CODE_CREATE_DOC);ACTION_CREATE_DOCUMENT会打开系统文件选择器让用户自己选位置。拿到返回值后用contentResolver.openOutputStream(uri)写入数据不再直接操作文件路径。这条改造适合任何涉及“导出备份”的Android源码。记账APP最怕数据导不出来用户在手机上记了一年的账没备份换手机就全没了所以省掉这一步就等于功能白做。4.5 真机安装提示签名不一致debug.keystore不是你的现象源码在Android Studio里直接Run能装到模拟器但从别人电脑上拷来的debug.keystore或者应用商店上架的签名文件和你本地的不一致导致真机覆盖安装时报“应用未安装”或“签名不一致”。原因Android Studio默认用~/.android/debug.keystore自动签名但换电脑后这个文件变了。解决自己生成一个固定的签名文件在app/build.gradle里配置signingConfigs让团队或你自己以后所有环境都用同一个keystore。5. 把记账APP源码改成能用一年的工具CSV自动备份、月预算预警与Room迁移5.1 CSV自动备份用WorkManager半个月拉一次别再用手动按钮记账APP的源码里通常都有“导出CSV”的按钮但手动导出这功能形同虚设。老账本的本机CSV备份依赖用户自觉真正要扛数据丢失还得靠自动定时。用WorkManager是最省心的方案。在APP启动时Schedule一个PeriodicWorkRequest时间间隔设成14天后台running的Worker里查bill表用CSV格式写入应用专属目录或MediaStore的公共目录然后把文件路径记录到SharedPreferences。PeriodicWorkRequest backupRequest new PeriodicWorkRequest.Builder(BackupWorker.class, 14, TimeUnit.DAYS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1, TimeUnit.DAYS) .build(); WorkManager.getInstance(context).enqueueUniquePeriodicWork( backup_work, ExistingPeriodicWorkPolicy.KEEP, backupRequest);WorkManager的14天周期不是精确的14天系统会对所有后台任务做批量调度可能会推迟几个小时所以不要按月备份设成30天。enqueueUniquePeriodicWork里的KEEP策略保证你不会在每次启动时重复创建多个备份任务。BackupWorker里执行完导出后记得返回Result.success()失败也别直接Result.failure()否则系统会按退避策略反复重试长时间占用CPU。5.2 月预算超支预警LiveData ViewModel处理“预算花超了”源码里如果直接用findViewById拿ProgressBar设置进度也能实现预警但一旦数据刷新频率高或者统计页和首页都要消费同一份预算数据就会代码重复。更干净的做法是用ViewModel持有月份和预算金额通过LiveData通知两个页面UI更新。public class BudgetViewModel extends ViewModel { private final MutableLiveDataLong budget new MutableLiveData(); private final MutableLiveDataLong spent new MutableLiveData(); private final MutableLiveDataBoolean overrun new MutableLiveData(); public void loadMonthData(int year, int month) { new Thread(() - { long spentThisMonth db.queryMonthSpent(year, month); long budgetAmount prefUtil.getMonthBudget(year, month); spent.postValue(spentThisMonth); budget.postValue(budgetAmount); overrun.postValue(spentThisMonth budgetAmount); }).start(); } }把数据库查询放进子线程避免主线程做I/O被StrictMode警告。postValue在子线程里更新LiveDatasetValue只能在主线程。overrun是一个布尔值首页观察到这个值变成true时把进度条颜色变红并弹Toast“本月已超支”。这个模式比onResume里查询、setText、setColor一条龙好维护得多改完预算预警后你会回头想把这些源码里所有的Activity交互都迁移到ViewModel上。5.3 从手写SQLiteHelper迁到Room什么时候做才不亏记账APP源码拿到手先别急着换Room如果你的目标是跑通手写SQLite完全够。但当你开始做前面提到的ViewModel迁移时你会发现手写SQLite的DAO层查询散落在Activity和多个类里每个查询都要写Cursor遍历代码重复度很高。这时候往Room迁是值得的。第一步引入依赖你的Entity类直接对应BillModelDAO接口写三个方法分别是插入账单、查全部流水、按月统计支出。第二步创建RoomDatabase实例然后手写一个迁移类把原来SQLiteHelper建的表结构换成Entity标注的表结构。Room没有自动把旧表数据搬进新表的能力所以你要在Migration里保留旧表、创建新表、复制数据、删旧表。static final Migration MIGRATION_1_2 new Migration(1, 2) { Override public void migrate(SupportSQLiteDatabase database) { database.execSQL(CREATE TABLE bill_new ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, type INTEGER NOT NULL, amount INTEGER NOT NULL, category TEXT NOT NULL, date TEXT NOT NULL, note TEXT, create_time TEXT DEFAULT CURRENT_TIMESTAMP)); database.execSQL(INSERT INTO bill_new (id, type, amount, category, date, note, create_time) SELECT id, type, CAST(amount * 100 AS INTEGER), category, date, note, create_time FROM bill); database.execSQL(DROP TABLE bill); database.execSQL(ALTER TABLE bill_new RENAME TO bill); } };这个迁移过程同时解决了第3章提到的金额精度问题把REAL类型的amount变成INTEGER并且乘以100存入新表。写在Migration里而不是在Activity启动时手动处理这样所有升级到这个版本的用户都会自动完成数据修正不需要重复提示用户“请在设置中备份数据”。迁移代码写完一定要先在模拟器上装一个旧版本录入几条真实数据再升级安装新版本确认数据没有丢再做真机验证。做完这些改造这套个人记账APP源码在你的电脑上才真正变成“你自己的项目”数据能备份、预算能预警、代码分层能继续加功能。我做完这些改造后最大的感触是与其去搜新的记账软件不如把自己手头的源码改顺手。记账的痛点在坚持而坚持的前提是打开APP几秒内能完成记录并且月底不需要对着表格发愁。这一点这份源码稍微打磨就能做到。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网