尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于Android Studio的课堂点名签到App开发实践

基于Android Studio的课堂点名签到App开发实践 简介移动应用开发中数据存储与界面交互是构建工具类App的核心能力。Android原生开发凭借SQLite轻量级数据库和RecyclerView高效列表组件能快速实现本地数据管理与流畅交互体验。本文从环境配置、Gradle构建到数据库表设计系统讲解如何使用Android Studio开发课堂点名签到应用涵盖随机点名逻辑、出勤状态标记、历史记录统计与CSV导出等高频场景。通过真实踩坑记录解决SDK配置、数据迁移、列表性能优化等典型问题帮助初学者掌握完整的原生App开发链路并应用于日常教学管理提升课堂效率。 课堂点名这件事干过助教或者代课的朋友都懂——一节课几十号人花名册念一遍就得两分钟碰上那种名字里带生僻字的更是尴尬。后来我发现不少老师干脆拍照签到、或者让学生自己报数结果代签、漏签满天飞。所以我用Android Studio自己动手写了一个基于安卓的课堂学生点名签到app专门解决“随机点名提问”和“出勤统计”这两个高频场景。这个项目不复杂但覆盖了安卓开发里非常核心的一条链路界面搭建、本地存储、列表渲染、事件交互、打包调试。如果你是刚学安卓开发的新手想找一个完整项目练手或者你是真被点名这事烦到不行这篇内容都值得你看完——我会把从环境配置到功能实现的完整过程都写出来包括我踩过的那些坑。1. 项目整体设计与功能拆解1.1 一节课点名到底需要哪几个功能在动手写代码之前我先把需求理了一遍。点名签到本质上就三件事名单从哪来、怎么点、结果怎么存。围绕这三件事我当时定了这么几个核心功能学生名单管理支持录入学生姓名和学号也可以按班级整体导入免去一个个手敲的麻烦。随机点名从名单中随机抽取一个学生用于课堂提问或者抽人回答问题。抽过的学生可以标记为“已点过”避免一节课总抽到同一个人。签到/缺勤标记上课后手动勾选到场学生快速标记迟到、请假、缺勤等状态。签到记录存储每次点名或签到都能留下记录支持按日期查看历史数据。数据统计与导出按周或按月统计出勤率方便期末汇总平时分。功能定完技术层面就清晰了。数据量不大不需要搞网络服务器用安卓自带的SQLite数据库就够了界面用原生控件加RecyclerView列表点名动画用Handler延时刷新就可以实现不需要引入第三方库。整个项目控制在单机本地运行不依赖网络上课时开着手机就能用。1.2 技术选型为什么用原生Android而不是其他方案可能有人会问现在跨平台方案那么多Flutter、Uniapp都能做为什么我偏选原生Android Studio我的理由其实很朴素这是一个典型的数据型工具应用原生开发最稳踩坑成本最低。原生安卓开发有几个对这个项目特别友好的点。首先是SQLite支持非常成熟不需要额外引入数据库框架直接用SQL语句操作逻辑简单清晰。其次是系统控件和权限管理比较直接比如读写本地文件导出CSV统计表在原生环境里就是几行代码的事。再就是Android Studio自带的调试工具尤其是布局检查和数据库查看器对排查问题特别有帮助。当然如果你要同时上架iOS并且就是不喜欢Java/Kotlin那选跨平台没毛病。但就“课堂点名签到”这种使用场景来说目标设备基本是老师和学生手里的安卓手机原生方案完全够用而且还省去了一层框架调度的性能损耗。1.3 功能模块与页面规划我按“最少页面完成闭环”的原则把app拆成了三个主要界面页面核心功能关键交互首页班级列表展示所有班级支持新建班级和班级管理点击进入点名页长按删除班级点名/签到页展示学生名单随机点名批量标记签到状态按钮点名点学生项切换状态统计/记录页查看历史签到记录按日期筛选导出统计列表展示支持导出CSV文件这个结构算是非常克制的了。一开始我也想过加“课程表管理”“学生照片头像”这些花哨功能后来都砍了——工具的命门是好用不是复杂。安卓开发里最忌讳的就是在MVP阶段功能膨胀会导致调试工作量翻倍还容易把新手劝退。2. 开发环境搭建与工程初始化2.1 Android Studio 安装与SDK配置这个项目用的是Android Studio目前新版基本都是随包内置了JDK17安装完就能直接用。但有几个配置容易踩坑我单独拎出来说。第一是下载版本的选择。官网下载页会提供Windows、macOS、Linux三个版本Windows版又分exe安装版和zip绿色版。我建议用exe安装版虽然要跑一遍安装向导但环境变量和SDK路径它会帮你配好省心。zip版适合追求极致洁癖的开发者但不适合新手。第二是SDK Platforms的勾选。安装完成后第一次启动Android Studio会提示下载SDK。这里建议至少勾选你手机系统版本对应的Platform比如手机是Android 13就勾选API 33同时把Android SDK Platform-Tools和Android SDK Build-Tools一并装上。这两个工具后面调试真机会用到少了它adb命令都跑不起来。第三是系统映像System Image。如果你不打算用真机想靠模拟器跑这一步就得选一个适合你电脑配置的系统镜像优先选x86_64架构的。老电脑开模拟器又慢又卡的话也不用硬扛后面我会讲怎么用真机调试体验会好很多。提示Android Studio首次启动时如果卡在下载组件界面多半是网络问题。可以把代理设置里的自动检测关掉或者换一个时段重试。别问我为什么知道问就是等过一小时。2.2 工程结构与Gradle配置要点新建项目时模板选Empty Views Activity千万别选Empty Activity那个是Jetpack Compose的新模板布局方式和传统XML差别很大不适合这个项目后面要讲的传统开发方式。包名我用的是com.example.classroomrollcall这个可以按你自己习惯改注意不要带中文。项目创建好后核心的Gradle配置在build.gradleModule级别文件里。我当时的配置大概是这样的android { namespace com.example.classroomrollcall compileSdk 34 defaultConfig { applicationId com.example.classroomrollcall minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.recyclerview:recyclerview:1.3.2 }几个参数我解释一下compileSdk是编译用的SDK版本minSdk是最低支持版本我定21是Android 5.0覆盖了绝大多数存量设备targetSdk是目标平台版本这个值会影响系统对App的兼容行为比如权限申请方式。这里有一个新手容易踩的坑——compileSdk和targetSdk建议保持相同数字不要一个34一个33混搭否则部分依赖库会在编译期给你报警告。2.3 模拟器与真机调试选择模拟器跑这个项目完全没问题但前提是你电脑配置别太老。如果你手头正好有一台安卓手机我强烈建议直接真机调试理由有三GPS、摄像头、传感器这些先不说单是列表滑动的手感和按钮点击的响应模拟器和真机的体验差距就非常大真机能直接测试手机厂商定制ROM的兼容性而且USB调试省掉了模拟器启动的漫长等待。真机调试步骤很简单但有几个品牌手机需要额外操作打开手机「设置」→「关于手机」连续点击「版本号」7次直到提示已进入开发者模式。回到设置页进入「开发者选项」打开「USB调试」。用数据线连接电脑手机会弹出授权对话框勾选“始终允许”后确认。在Android Studio顶部工具栏选择你的设备点击Run按钮。注意部分品牌比如某些定制系统在连接电脑后默认是“仅充电”模式需要在手机通知栏把USB模式切换为“文件传输”或“传输文件”否则adb识别不到设备。# 验证设备是否连接成功 adb devices看到设备ID加device字样就说明连接成功了。如果显示unauthorized就拔掉数据线重新插并留意手机上的授权弹窗。2.4 几个高频环境问题的排查这里专门写一节环境排错因为我在学生群和论坛里看过太多人卡在这一步就放弃了。下面三个问题是最常见的问题一Android Studio提示SDK路径找不到。多半是SDK Manager里的路径包含中文或空格导致构建脚本无法解析。解决办法是重新指定一个纯英文路径比如D:\Android\Sdk然后在local.properties文件里添加sdk.dirD\:\\Android\\Sdk。问题二Gradle同步一直卡住或下载依赖超时。国内网络环境下访问Maven中央仓库偶尔会极慢。解决办法是配置阿里云镜像源在build.gradle的repositories块里加上阿里的仓库地址repositories { google() mavenCentral() maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/google } }问题三构建时报org.gradle.api.ProjectConfigurationException。这通常是Gradle版本和Android Gradle Plugin版本不匹配。可以打开File - Project Structure - Project把Android Gradle Plugin Version和Gradle Version调整成官方推荐的对应版本组合然后点击Sync Now重新同步。环境这块说句公道话安卓开发环境配置确实比后端写接口要繁琐不少百分之八十的问题都集中在SDK路径、Gradle版本映射、网络源这三件事上。把这三点理顺了后面写代码就是一路畅通。3. 数据库设计与核心功能逻辑3.1 数据库表结构设计点名签到app的数据结构比较固定我用SQLite落了三张表class_table存班级信息、student_table存学生信息、record_table存签到记录。-- 班级表 CREATE TABLE class_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, class_name TEXT NOT NULL, create_time TEXT DEFAULT (datetime(now, localtime)) ); -- 学生表 CREATE TABLE student_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, class_id INTEGER NOT NULL, student_no TEXT, student_name TEXT NOT NULL, FOREIGN KEY(class_id) REFERENCES class_table(id) ); -- 签到记录表 CREATE TABLE record_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, class_id INTEGER NOT NULL, student_id INTEGER NOT NULL, date TEXT NOT NULL, status INTEGER DEFAULT 0, FOREIGN KEY(student_id) REFERENCES student_table(id) );status字段我用整数存储0代表缺勤1代表已到2代表迟到。用整数而不是字符串的好处是查询统计时直接用SUM和COUNT函数做聚合计算效率高还不容易产生脏数据。这里有个设计细节我把日期字段存成了yyyy-MM-dd格式的文本。有人可能会说为什么不存时间戳因为签到只需要按天维度来统计文本格式可以直接用LIKE查询某一天的数据而且读取出来直接展示不需要格式化。只要你不做跨时区的复杂计算这个方案是够用的。3.2 学生名单的录入与导入名单录入我做了两种方式。第一种是手动添加适合就几十个人的小班第二种是文本批量导入适合上百人的大班。手动添加的界面逻辑很简单就是弹出一个对话框填姓名和学号点击确认后插入SQLite。批量导入则是读取一个纯文本文件每行格式是“学号,姓名”按逗号分割后循环插入。private void importStudentsFromFile(String filePath, long classId) { ListStudent studentList new ArrayList(); try { BufferedReader reader new BufferedReader(new FileReader(filePath)); String line; while ((line reader.readLine()) ! null) { line line.trim(); if (line.isEmpty()) continue; String[] parts line.split(,); if (parts.length 2) continue; String no parts[0].trim(); String name parts[1].trim(); studentList.add(new Student(classId, no, name)); } reader.close(); } catch (IOException e) { e.printStackTrace(); } databaseHelper.insertStudents(studentList); }这个导入功能我实际用下来觉得特别香。很多老师手里本身就有一份Excel表格导出成CSV格式再把内容复制到txt里就能直接导进来几分钟就搞定一个一百多人的班级名单。如果你想把Excel解析得更自动化可以考虑引入Apache POI库但为了一个小工具引入三四个依赖包我觉得没有必要。3.3 随机点名逻辑与防重复设计随机点名是这个app的灵魂功能。最简单的实现是Random类随机取一个下标但如果连续抽到同一个人体验就很差。我当时设计了一个“未点名单”和“已点名单”两个池子每次点名时先从全部学生中随机抽一名。抽中的学生进入“已点名单”并从未点名单中移除。当未点名单为空时自动重置全部学生为未点状态。public Student randomRollCall() { ListStudent pendingList getPendingStudents(); if (pendingList.isEmpty()) { resetAllPendingStatus(); pendingList getPendingStudents(); } int randomIndex new Random().nextInt(pendingList.size()); Student selected pendingList.get(randomIndex); markAsCalled(selected.getId()); return selected; }getPendingStudents()是查数据库里今天还没被点到过的学生markAsCalled则是更新记录表。用数据库存点名状态有一个好处就算用户按了Home键或者刷新了页面状态也不会丢。UI层面我做了一个滚动动画点名过程中名字在列表里快速跳转停下来的瞬间才锁定最终人选这样比直接闪现一个名字更有仪式感学生也会更专注。3.4 签到统计与数据导出签到统计看两个指标出勤率和迟到次数。出勤率计算公式是出勤率 已到人数 迟到人数/ 班级总人数 × 100%这个统计我在查询时直接用SQL聚合SELECT COUNT(*) AS total, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) AS present, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) AS late FROM record_table WHERE class_id ? AND date ?;拿到这几个值在页面里展示出来就行。导出功能我做了CSV格式目的是方便老师后续用Excel做二次分析。CSV本质是逗号分隔的纯文本文件生成不需要额外依赖库StringBuilder sb new StringBuilder(); sb.append(学号,姓名,到课状态\n); for (Record record : recordList) { sb.append(record.getStudentNo()).append(,) .append(record.getStudentName()).append(,) .append(getStatusText(record.getStatus())).append(\n); } File file new File(getExternalFilesDir(null), 签到记录_ date .csv); FileOutputStream fos new FileOutputStream(file); fos.write(sb.toString().getBytes(GBK)); fos.close();注意这里写入编码用了GBK而不是默认的UTF-8。因为CSV文件用Excel打开时UTF-8格式的中文很容易乱码GBK则能被Excel正确识别。这个细节如果你不处理导出的文件在用户手里的第一印象就会很糟糕。实操心得数据导出目录我用了getExternalFilesDir(null)。这个目录是应用专属的外部存储不需要申请存储权限而且应用卸载时会自动清理不会给用户手机留下垃圾文件。4. 界面布局与交互实现4.1 主界面和列表页的开发主界面我用RecyclerView来展示班级列表。为什么用RecyclerView而不是老掉牙的ListView——因为RecyclerView自带ViewHolder模式列表项复用的性能好很多而且支持线性布局、网格布局的自由切换。你之后如果想在主界面加一个“最近点名班级”的横滑区RecyclerView也能直接支持。RecyclerView的使用三步走写一个Adapter、写一个ViewHolder、设置LayoutManager。Adapter里最关键的onBindViewHolder方法负责把数据绑定到界面控件上Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { ClassInfo classInfo classList.get(position); holder.tvClassName.setText(classInfo.getClassName()); holder.tvStudentCount.setText(classInfo.getStudentCount() 人); holder.itemView.setOnClickListener(v - { Intent intent new Intent(context, RollCallActivity.class); intent.putExtra(classId, classInfo.getId()); intent.putExtra(className, classInfo.getClassName()); context.startActivity(intent); }); }这里有一个特别值得说的性能点onBindViewHolder里不要做耗时操作比如数据库查询、文件IO否则列表滑动时会明显卡顿。所以我在进入主界面时一次性把班级列表连同学生人数查好放进内存里的ArrayListonBindViewHolder只是单纯地读取内存数据。如果你想追求更极致的流畅度可以上DiffUtil做增量更新但班级列表这种小数据量完全没必要。4.2 点名页的设计点名动画与状态切换点名页的布局分了上下两块上半部分是当前被点中学生的信息展示区下半部分是全班学生的列表。动画效果我是用Handler的postDelayed做的“滚动姓名”效果private void startRollingAnimation() { handler new Handler(Looper.getMainLooper()); Runnable runnable new Runnable() { Override public void run() { int currentIndex random.nextInt(studentList.size()); tvDisplayedName.setText(studentList.get(currentIndex).getName()); if (rollingCount 15) { rollingCount; handler.postDelayed(this, 100); } else { // 动画结束锁定最终学生 Student finalStudent rollCallLogic.getFinalStudent(); tvDisplayedName.setText(finalStudent.getName()); updateListStatus(); } } }; handler.postDelayed(runnable, 100); }动画过程中每100毫秒换一个名字总共轮换15次一秒钟左右出结果。这个节奏是我反复试过的太短没有悬疑感太长会让学生不耐烦。而每次切换间隔选100毫秒是因为人的视觉暂留加上短暂期待感刚好能营造出“抽奖”的刺激感。点名页的下半部分列表我让每个学生的item通过背景色区分状态灰色是未点绿色是已到黄色是迟到红色是缺勤。这样老师扫一眼就知道今天哪些人还没处理不需要逐个人点开看。4.3 状态标记的交互与防误触设计在列表里点击某个学生的item会弹出状态选择对话框让老师选择“已到/迟到/缺勤/取消”。这里我用了一个小技巧默认状态下点击列表项直接标记为已到减少老师的操作次数如果需要改成迟到或缺勤再长按item打开状态选择框。实际使用下来默认点击直接标记效率能提升一倍不止。为了防止误触我做了两个保护措施。第一点名动画进行中禁用列表点击事件防止老师手滑把正在滚动中的名字点了第二标记状态后列表项会短暂显示一个反馈动画背景色渐变文字变化让老师明确知道操作已生效。// 防止动画期间误触 if (isRolling) { Toast.makeText(this, 点名进行中请稍候, Toast.LENGTH_SHORT).show(); return; }这些交互细节虽然不复杂但真影响使用体验。我见过不少app功能都正常就是按键容易误触、状态反馈不明显老师上课时操作起来特别别扭。工具类app一定要把“操作效率”和“防误触”放在设计核心位置。5. 开发过程中的典型问题与调试经验5.1 数据库版本升级与数据迁移这个项目开发到中期我加了一个字段。原本record_table只有status字段后来想记录“备注”比如“请假原因”“缺课课时”于是加了remark TEXT列。问题来了已经安装过的用户数据库里没有这一列直接用新代码查询就会报no such column错误。解决办法是用SQLite的onUpgrade回调做版本升级迁移Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE record_table ADD COLUMN remark TEXT DEFAULT ); } }同时把数据库版本号从1改成2。这样旧用户升级app时系统会自动调用onUpgrade在原有表结构上补充新列。注意不要图省事直接DROP TABLE重建那会把用户之前存的签到记录全清空属于数据事故。就算只在测试阶段养成写迁移脚本的习惯也很重要因为你永远不知道用户手机会在什么时候升级。5.2 随机点名重复与状态不一致的问题刚写完随机点名逻辑时我遇到过一个隐蔽的bug连点两次点名按钮同一个学生竟然被抽中了两次。排查了很久才发现问题出在pendingList的缓存上。我第一次点名时把“未点学生列表”加载到了内存点完被点中的学生只是从内存中移除了但数据库状态还没更新紧接着第二次点名又加载了一次数据库把这个学生查回来了。解决方式是把“查询未点学生”和“标记已点”改成同一个事务操作要么都成功要么都失败db.beginTransaction(); try { ListStudent pendingList db.queryPendingStudents(classId, today); if (pendingList.isEmpty()) { db.resetAllStatus(classId, today); pendingList db.queryPendingStudents(classId, today); } Student selected pendingList.get(random.nextInt(pendingList.size())); db.markAsCalled(selected.getId(), today); db.setTransactionSuccessful(); return selected; } finally { db.endTransaction(); }用事务把“查”和“改”绑定在一起才能保证并发场景下数据的一致性。这也是从这次bug里学到的教训涉及“先查再改”的逻辑一定要考虑中间状态的不可分割性。5.3 界面卡顿与列表性能优化我第一版列表加载是所有学生信息从数据库查询后直接在UI线程绑定班级人数一多比如实验班100多人点名页的滚动就显得有点生涩。后来做了两个优化效果立竿见影第一个优化是数据加载异步化。用Handler或AsyncTask把数据库查询放到子线程数据查完之后再通过runOnUiThread切回主线程更新UI。现在Kotlin可以用协程Java项目里我用的是ExecutorService。核心思想就一句话不要在UI线程里做数据库操作。executor.execute(() - { ListStudent students databaseHelper.getStudentsByClass(classId); runOnUiThread(() - { adapter.setData(students); adapter.notifyDataSetChanged(); }); });第二个优化是RecyclerView的setHasFixedSize(true)。如果列表的item高度固定可以调用这个方法RecyclerView就不需要每次重新测量高度滚动时会省掉布局计算的时间。5.4 “App抓包失败”与网络权限排查这个项目本身不需要网络但我在调试阶段发现一个现象项目里如果没声明INTERNET权限某些依赖库比如崩溃统计SDK会静默失效如果声明了但用的是明文HTTP请求在Android 9.0以上系统默认会被拦截日志里报“Cleartext HTTP traffic not permitted”。!-- AndroidManifest.xml -- application android:usesCleartextTraffictrue ...如果你做的是需要调试接口的版本临时加上这一行能解决大部分抓包失败的问题。但这只是一个妥协方案正式上线时建议还是用https协议或者用networkSecurityConfig只允许特定域名的明文流量。这个知识点算是我用“抓包失败”这个血泪经历换来的。6. 项目扩展方向与上架经验6.1 功能增强方向基础版功能完整跑通之后可以按实际需要做扩展。我这里列几个我自己想加但还没加的方向给有兴趣继续做的朋友参考蓝牙点名教室场景下可以让学生手机开蓝牙老师端扫描学生设备的蓝牙广播实现“一键签到”。技术上用BluetoothLeScanner或者经典蓝牙的DiscoveryReceiver就可以但耗电和兼容性测试会比较烦。本地通知提醒用AlarmManager设置上课前5分钟提醒基础版的提醒下发已经比较成熟做起来不复杂。CSV导出增加数据透视把多次签到记录合并成“单个学生多日汇总表”统计每个人的出勤率波动趋势这个对期末考核很有用。投屏模式通过无线投屏或者Chromecast把点名界面投射到教室大屏上滚动点名的时候全班都能看到。这个比较炫但需要处理生命周期和多设备状态同步复杂度直线上升。记住一条原则新功能必须回到“辅助课堂管理”这个核心目的不做跟点名签到无关的泛娱乐功能。6.2 上架应用市场的踩坑清单项目做完之后如果想让更多人用可以考虑上架应用市场。国内主流的安卓应用市场包括应用宝、华为、小米、OPPO、vivo等每个市场的审核要求不完全一样但有几个通用注意事项签名配置正式上架必须使用自己的签名文件不能使用Android Studio调试时的debug签名。生成签名文件用Build - Generate Signed Bundle / APK选APK新建一个Key Store填好别名和密码。签名一旦生成就要妥善保管丢了你永远没法更新已上架的应用。隐私政策应用如果涉及个人信息存储这个app存了学生姓名学号严格来说属于个人信息市场审核基本都要求提供隐私政策链接。我是在云服务器上挂了一个简单的静态页面内容包括收集哪些信息、如何使用、如何删除。targetSdkVersion各大市场都要求新上架应用targetSdk不能太低最低要求基本是30以上。我上面配置的是34所以问题不大。如果targetSdk太低部分市场会直接拒绝上架。64位架构支持从2021年起主流市场都要求应用包必须包含64位so库。如果你没用到NDK和so库纯Java/Kotlin开发通常默认就支持64位如果使用了第三方SDK要留意SDK是否包含arm64-v8a目录。6.3 开发周期复盘与个人经验小结最后分享一点我自己的开发心得。这个项目从零开始到功能完整可体验我大概用了两周的业余时间其中花在环境搭建和排错上的时间占了一小半真正写业务代码反而很快。这恰恰说明安卓开发的初期门槛主要在环境而不是逻辑。保持耐心踩着坑往前走每个报错信息都是一次学习机会。我个人在实际操作中最大的体会是做一个工具型app最核心的能力不是写代码而是判断“什么该做、什么不该做”。我在最开始就把“随机点名”“签到标记”“数据统计”这三个功能死死咬住砍掉了一切可做可不做的功能才让这个项目在一周内就跑通了全部闭环。如果你也想自己动手写一个类似的app建议你也能忍住加功能的冲动先把核心流程做扎实再去想怎么添砖加瓦。后面我还在琢磨怎么把Excel名单的解析做得更自动化一点或者生成更直观的可视化出勤图表打算在下一个版本里试一下。本文还有配套的精品资源点击获取
返回列表