在大学校园里,传统的纸质考勤和作业收集方式正面临诸多痛点。作为一名在高校信息化建设领域工作多年的开发者,我亲眼目睹了老师们每节课花10分钟点名、课后整理作业时的混乱场景。教务员统计考勤数据时经常需要手动录入Excel表格,一个学期下来光是考勤表就能堆满半个档案柜。
这个Android应用正是为了解决这些实际问题而设计的。它需要实现三个核心功能:
采用Android Jetpack组件构建应用框架:
考虑到不同院系的网络条件差异,特别设计了离线模式:
kotlin复制class AttendanceViewModel : ViewModel() {
private val _attendanceState = MutableStateFlow<SyncState>(SyncState.IDLE)
val attendanceState: StateFlow<SyncState> = _attendanceState
suspend fun recordAttendance(location: GeoPoint) {
if(!NetworkMonitor.isOnline()) {
localDB.insertPendingAttendance(location)
_attendanceState.value = SyncState.PENDING
} else {
// 实时同步逻辑
}
}
}
后端采用微服务架构:
使用Spring Cloud Gateway作为API网关,配合JWT实现认证授权。特别针对高并发签到场景做了优化:
为防止代签作弊,系统实现了三重防护机制:
核心生成逻辑:
java复制public class QRGenerator {
private static final int EXPIRE_SECONDS = 60;
public String generate(String courseId, String teacherId) {
String rawText = String.join("|",
courseId,
teacherId,
String.valueOf(System.currentTimeMillis() / (EXPIRE_SECONDS * 1000))
);
return AESUtil.encrypt(rawText, getDailyKey());
}
// 每天更换的密钥
private static String getDailyKey() {
return DigestUtils.md5Hex(
LocalDate.now().format(DateTimeFormatter.ISO_DATE) +
System.getenv("QR_SALT")
);
}
}
针对大文件上传不稳定的问题,开发了分片上传方案:
关键代码实现:
kotlin复制class UploadWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val fileUri = inputData.getString("file_uri") ?: return Result.failure()
val file = File(URI.create(fileUri))
val totalSize = file.length()
val chunkSize = 2 * 1024 * 1024 // 2MB
val chunks = ceil(totalSize.toDouble() / chunkSize).toInt()
val md5 = calculateMD5(file)
val uploaded = api.getUploadedChunks(md5)
file.inputStream().buffered().use { stream ->
repeat(chunks) { index ->
if (index !in uploaded) {
val buffer = ByteArray(chunkSize)
val bytesRead = stream.read(buffer)
api.uploadChunk(md5, index,
if(bytesRead == chunkSize) buffer
else buffer.copyOf(bytesRead)
)
}
}
}
return Result.success()
}
}
现象:部分学生在教室内签到却显示定位失败
排查过程:
java复制public static boolean isLocationMockEnabled(Context ctx) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return ctx.getSystemService(LocationManager.class)
.isLocationMockEnabled();
} else {
return Settings.Secure.getInt(ctx.getContentResolver(),
Settings.Secure.ALLOW_MOCK_LOCATION, 0) != 0;
}
}
解决方案:
现象:教师端收到的.docx文件无法打开
根本原因:
改进方案:
javascript复制function checkFileMagic(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = e => {
const arr = new Uint8Array(e.target.result.slice(0, 4))
const header = Array.from(arr).map(b => b.toString(16))
resolve(header.join('').toUpperCase())
}
reader.readAsArrayBuffer(file.slice(0, 4))
})
}
// 检查PDF文件头
const pdfHeader = await checkFileMagic(file)
if(pdfHeader !== '25504446') {
throw new Error('Invalid PDF file')
}
当课程人数超过200人时,完整加载考勤记录会导致UI卡顿。解决方案:
优化后的ViewModel配置:
kotlin复制class AttendanceViewModel(private val courseId: String) : ViewModel() {
val attendanceList = Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { AttendancePagingSource(courseId) }
).flow.cachedIn(viewModelScope)
}
class AttendancePagingSource(private val courseId: String) : PagingSource<Int, Attendance>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Attendance> {
return try {
val page = params.key ?: 0
val response = api.getAttendances(courseId, page, params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 0) null else page - 1,
nextKey = if (response.isLast) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
针对艺术类课程常见的图片作业,实施智能压缩:
使用Android自带的压缩API:
java复制public File compressImage(File original) throws IOException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(original.getPath(), options);
int scale = calculateScale(options.outWidth, options.outHeight);
options.inJustDecodeBounds = false;
options.inSampleSize = scale;
Bitmap scaledBitmap = BitmapFactory.decodeFile(original.getPath(), options);
File output = new File(getCacheDir(), "compressed.webp");
try (FileOutputStream out = new FileOutputStream(output)) {
scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 80, out);
}
return output;
}
private int calculateScale(int width, int height) {
int maxDimension = Math.max(width, height);
return maxDimension > 1920 ? maxDimension / 1920 : 1;
}
所有数据库操作都采用参数化查询:
额外防护措施:
实现沙箱式文件存储:
存储目录结构示例:
code复制/uploads
/course_101
/assignment_1
/user_1001
document.pdf.sha256
/user_1002
image.webp.sha256
/quarantine
infected_file.zip
利用MPAndroidChart实现可视化:
核心统计SQL:
sql复制SELECT
strftime('%Y-%m-%d', datetime(create_time/1000, 'unixepoch')) as day,
COUNT(CASE WHEN status = 'NORMAL' THEN 1 END) * 100.0 / COUNT(*) as rate
FROM attendance
WHERE course_id = ?
GROUP BY day
ORDER BY day
基于NLP的作业内容分析:
Python分析服务示例:
python复制def analyze_submissions(assignment_id):
submissions = db.get_submissions(assignment_id)
texts = [preprocess(s.content) for s in submissions]
vectorizer = TfidfVectorizer(max_features=50)
tfidf_matrix = vectorizer.fit_transform(texts)
# 计算相似度矩阵
cosine_sim = cosine_similarity(tfidf_matrix)
# 标记相似度超过阈值的作业
for i in range(len(cosine_sim)):
for j in range(i+1, len(cosine_sim)):
if cosine_sim[i][j] > 0.8:
db.flag_similar(i, j)
针对国内Android生态的推送服务碎片化问题:
推送路由逻辑:
kotlin复制fun pushNotification(user: User, message: PushMessage) {
when {
Build.MANUFACTURER.equals("xiaomi", ignoreCase = true) -> {
MiPushClient.sendMessage(user.miPushId, message)
}
Build.MANUFACTURER.equals("huawei", ignoreCase = true) -> {
HmsPushService.send(user.hmsToken, message)
}
else -> {
FirebaseMessaging.getInstance().send(
RemoteMessage.Builder("${user.id}@fcm")
.setData(message.toMap())
.build()
)
}
}
}
遵循Material Design设计规范:
主题配置示例:
xml复制<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- 深色模式覆盖 -->
<item name="colorSurface" tools:targetApi="q">?attr/colorBackground</item>
</style>
在代码中监听主题变化:
kotlin复制class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AppCompatDelegate.setDefaultNightMode(
if (Prefs.darkMode) MODE_NIGHT_YES
else MODE_NIGHT_NO
)
delegate.addOnConfigurationChangedListener { config ->
if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES) {
// 更新深色模式相关UI
}
}
}
}
采用分层测试策略:
使用MockK进行单元测试:
kotlin复制@Test
fun `test attendance recording`() = runTest {
val mockRepo = mockk<AttendanceRepository>()
coEvery { mockRepo.recordAttendance(any()) } returns Result.success()
val vm = AttendanceViewModel(mockRepo)
vm.recordAttendance(GeoPoint(39.9, 116.4))
coVerify { mockRepo.recordAttendance(any()) }
assertTrue(vm.attendanceState.value is SyncState.SUCCESS)
}
覆盖主流设备和系统版本:
构建Gradle任务自动执行测试:
groovy复制android {
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
animationsDisabled true
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "failed", "standardOut"
}
}
}
}
task fullTest(type: GradleBuild) {
tasks = [
'clean',
':app:testDebugUnitTest',
':app:connectedDebugAndroidTest',
':app:lintDebug'
]
}
分阶段上线方案:
使用Firebase Remote Config控制发布:
java复制FirebaseRemoteConfig config = FirebaseRemoteConfig.getInstance();
config.fetchAndActivate().addOnCompleteListener(task -> {
if (task.isSuccessful() && config.getBoolean("enable_new_feature")) {
// 启用新功能
}
});
关键监控指标:
Prometheus监控配置示例:
yaml复制scrape_configs:
- job_name: 'attendance_service'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['service:8080']
relabel_configs:
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: service
告警规则设置:
yaml复制groups:
- name: attendance.rules
rules:
- alert: HighErrorRate
expr: rate(http_server_requests_errors_total[1m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate on {{ $labels.instance }}"
正在测试的改进方案:
使用ML Kit实现:
java复制FaceDetectorOptions options = new FaceDetectorOptions.Builder()
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_ALL)
.enableTracking()
.build();
FaceDetector detector = FaceDetection.getClient(options);
detector.process(inputImage)
.addOnSuccessListener(faces -> {
if (faces.size() == 1) {
Face face = faces.get(0);
float smileProb = face.getSmilingProbability();
// 活体检测逻辑
}
});
实验性功能开发:
使用ANTLR实现简单题目批改:
java复制public class MathGrader {
public static double grade(String answer, String standard) {
MathParser parser = new MathParser();
Node studentTree = parser.parse(answer);
Node teacherTree = parser.parse(standard);
return new TreeComparator().compare(studentTree, teacherTree);
}
}
这个项目从第一行代码到现在已经迭代了两年多,期间我们处理过各种意想不到的情况:从EMUI系统的后台限制导致推送无法送达,到某型号平板的GPS模块存在固件缺陷。最大的体会是教育类应用必须兼顾技术创新和用户体验,任何功能设计都要考虑实际教学场景中的使用习惯。比如老教授们更习惯传统的点名方式,我们就保留了手动补签功能;艺术类作业文件体积大,就专门优化了上传体验。