2026-07-13 取餐记录定时补传实现
背景
原取餐记录在取餐结束后实时上传,失败时只在当前流程内重试,最终进入 FAILED 后没有后台消费者继续补传。网络中断、App 重启或请求临时异常时,记录可能长期留在本地。
本次按照已确认方案,仅修改 Android 端,不修改 /p/api/zhctPushMeal 接口字段和服务端逻辑。
实现内容
固定周期与单批上限
- App 初始化后安排取餐记录补传闹钟。
- 使用
AlarmManager固定每 3 分钟触发一次检查。 - 闹钟每次触发后立即续订下一次。
- 无网络时跳过本轮,不选取记录、不修改记录状态和重试次数。
- 有网络时提交唯一一次性 WorkManager,单轮最多选取 20 条。
- 按
pick_id ASC处理,严格等待上一条返回后再处理下一条。 - 使用
ExistingWorkPolicy.KEEP,上一轮未结束时不启动第二轮。
状态与重试规则
新增并统一使用以下状态:
PENDING:等待上传。UPLOADING:已原子占用,正在请求。UPLOADED:服务端明确返回code=0。FAILED:本次失败,可以继续补传。FAILED_FINAL:累计失败 10 次,停止自动补传。REJECTED:服务端明确返回code=1,停止补传。IGNORED:取餐重量小于 5g 或菜品 UUID 无效,不上传。
实时上传继续最多尝试 3 次,失败次数计入同一个 retry_count;周期补传中每条记录每轮只请求一次,单条失败但网络仍可用时继续处理下一条。
数据库升级
CustomDBHelper 数据库版本从 2 升至 3。
取餐记录表新增:
retry_countlast_attempt_timelast_error_codelast_error_messageupload_updated_at
新增 pick_record_upload_log 追加日志表,记录批次 ID、触发来源、前后状态、尝试次数、请求起止时间、结果码、错误分类和错误摘要。
增加以下保护:
PENDING/FAILED -> UPLOADING使用数据库条件更新原子占用。- 实时上传和周期补传同时命中同一条记录时,只有占用成功的一方能够发起请求。
UPLOADING超过 10 分钟自动恢复为FAILED,恢复动作不增加失败次数。- 查询索引为
(upload_status, retry_count, pick_id)。
上传链路统一
- 新增
PickMealUploadCoordinator,统一实时上传和周期补传的请求、状态变更与日志落库。 - 新增纯 Java
PickMealRetryProcessor,负责 20 条上限、升序、严格串行、失败继续和断网停止。 WeightMainActivity不再自行递归上传和直接覆盖状态。- 小于 5g 的记录由原来的“标记已上传”调整为
IGNORED。 - 修正
ApiResultUtils在响应data=null时强制把业务码改成 0 的问题,现保留服务端真实业务码;空响应改为网络读写异常,不再误判成功。
主要代码文件
app/src/main/java/com/zhct/traybinding/upload/PickMealUploadPolicy.javaapp/src/main/java/com/zhct/traybinding/upload/PickMealUploadResult.javaapp/src/main/java/com/zhct/traybinding/upload/PickMealRetryProcessor.javaapp/src/main/java/com/zhct/traybinding/upload/PickMealUploadCoordinator.javaapp/src/main/java/com/zhct/traybinding/database/PickMealUploadStore.javaapp/src/main/java/com/zhct/traybinding/workmanager/PickMealRetryWorker.javaapp/src/main/java/com/zhct/traybinding/utils/PickMealRetryScheduler.javaapp/src/main/java/com/zhct/traybinding/database/CustomDBHelper.javaapp/src/main/java/com/zhct/traybinding/database/PickMealRecordImpl.javaapp/src/main/java/com/zhct/traybinding/bean/PickMealRecord.javaapp/src/main/java/com/zhct/traybinding/receiver/AlarmReceiver.javaapp/src/main/java/com/zhct/traybinding/application/CptApplication.javaapp/src/main/java/com/zhct/traybinding/main/WeightMainActivity.javaapp/src/main/java/com/zhct/traybinding/network/ApiResultUtils.java
测试
新增 JUnit 测试:
- 5g 边界和无效菜品判断。
code=0/1/其他的成功、拒绝、失败分类。- 第 10 次失败进入
FAILED_FINAL。 - 无网络时不加载、不占用、不上传。
- 25 条积压时只处理最早 20 条。
- 严格按照 ID 升序同步处理。
- 单条失败后继续下一条。
- 中途断网后停止,剩余记录不占用。
- 无效记录进入
IGNORED且不调用上传器。
验证记录
基线验证:
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash ./gradlew :app:testDebugUnitTest :app:compileDebugJavaWithJavac --no-daemon
结果:修改前 BUILD SUCCESSFUL in 22s。
TDD RED:新增测试后,生产策略类尚未实现,:app:compileDebugUnitTestJavaWithJavac 按预期失败,错误为找不到 PickMealUploadPolicy、PickMealRetryProcessor 和 PickMealUploadResult。
TDD GREEN:
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash ./gradlew :app:testDebugUnitTest --no-daemon
结果:BUILD SUCCESSFUL in 19s。
Java 编译:
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash ./gradlew :app:testDebugUnitTest :app:compileDebugJavaWithJavac --no-daemon
结果:BUILD SUCCESSFUL in 19s。
数据库迁移 SQL 使用内存 SQLite 验证,v3 主表为 17 列,上传日志表为 13 列,索引创建成功。
APK 构建:
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash ./gradlew :app:assembleDebug --no-daemon
结果:BUILD SUCCESSFUL in 59s,输出 app/build/outputs/apk/debug/app-debug.apk。
最终验证门:
JAVA_HOME=$(/usr/libexec/java_home -v 1.8) bash ./gradlew -q :app:testDebugUnitTest :app:assembleDebug --no-daemon
git diff --check
结果:命令退出码为 0;JUnit 共 10 项测试,failures=0、errors=0;debug APK 大小约 59MB;git diff --check 无输出。
风险与设备验收
- 服务端没有幂等键,超时或断电发生在服务端已入库、本地尚未更新状态时,仍可能出现重复记录。本实现采用“至少一次”语义,优先防止漏传。
- Android 休眠策略可能影响精确闹钟的实际触发时间;目标设备常亮运行时应按 3 分钟执行,仍需设备日志确认。
- 本轮已完成单元测试、迁移 SQL、Java 编译和 APK 构建,尚未在目标设备上制造断网、21 条/45 条积压和进程中断场景。
设备验收建议:
- 准备 21 条
FAILED,确认第一轮最多 20 条、下一轮 1 条。 - 补传过程中断网,确认未处理记录保持原状态。
- 将一条记录置为超时
UPLOADING,确认恢复为FAILED。 - 核对
pick_record_upload_log中每次占用和结果状态成对出现。 - 确认历史
UPLOADED记录不进入补传队列。