OnecardDemo 用户信息 SQLite 分批持久化设计
日期:2026-08-24
状态:已实施,待现场全量同步验证
1. 目标与已确认规则
“获取全部用户信息”继续按每批最多 1000 条调用 GetCustomerByNum,但不再把全部 SOAP XML 留在内存并显示到页面。每个请求成功并完成解析后,立即在单独事务中写入应用私有 SQLite 数据库。
已确认的业务规则:
- 所有服务端原始字段都保存,包括
PWD、QUERYPWD、IDCARDNO。 CUSTOMERID是用户唯一键;重复账号执行更新,不插入重复用户。- 每批成功后立即提交,后续批次失败不回滚此前已经成功保存的批次。
- 全量同步全部成功后才清理服务端已经不存在的旧用户;中途失败时不删除旧用户。
2. 真实响应结构证据
2026-08-24 使用现场 WebService、AppID 6 调用 GetCustomerByNum(custid=0, num=2),只检查 XML 结构和字段名,不记录字段值。响应包含 2 个 PROXY_BASE_CUSTOMERS 用户节点,每个节点当前包含 51 个字段:
CUSTOMERID、OUTID、SCARDSNR、CARDNO、CARDSN、BANKCARDNO、STATUS、OPENDT、NOUSEDATE、PWD、OPCOUNT、CARDFUNC、CARDTYPE、CARDSFID、CUSTDEPT、EMPCODE、SUMFARE、ODDFARE、SUBODDFARE、ODDFAREACC、SUBODDFAREACC、SUMCONSUMEFARE、SUMADDFARE、SUMSUBSIDY、SUMQC、SUMLOAD、SUMSAVE、SUMSXF、IFPUSHFARE、NAME、JPDM、SEX、CERTIFICATEID、IDCARDNO、SERVERID、MEDICALTYPE、REGSTARTYEAR、VER、QUERYPWD、PHOTOID、ISBIGFARECARD、COUNTRY、NATION、SUBOPCOUNT、SUBSAVEOPCOUNT、SPECIALOPCOUNT、SAVEOPCOUNT、SPECIALODDFARE、CARDKIND、CARDOPERTYPE、CardBlock。
字段集合可能随服务端版本变化,因此数据库不能只建立固定的 51 个业务列。
3. 数据库结构
数据库文件为应用私有目录中的 onecard.db,数据库版本从 1 开始,使用 Android 系统 SQLiteOpenHelper,不增加 Room 或第三方数据库依赖。
3.1 用户主表 customers
| 字段 | 类型 | 规则 |
|---|---|---|
customer_id |
INTEGER | 主键,对应原始 CUSTOMERID |
card_no |
TEXT | 原始 CARDNO,建立普通索引 |
out_id |
TEXT | 原始 OUTID,建立普通索引 |
name |
TEXT | 原始 NAME |
status |
TEXT | 原始 STATUS |
last_seen_sync_id |
TEXT | 最后一次看到该用户的同步批次 ID |
updated_at |
INTEGER | 本机保存时的毫秒值 |
主表保存后续业务最常用的查询字段,所有值保持服务端原始字符串;除了 customer_id 用于唯一性外,不对金额、日期、状态进行业务转换。
3.2 原始字段表 customer_fields
| 字段 | 类型 | 规则 |
|---|---|---|
customer_id |
INTEGER | 关联 customers.customer_id |
field_name |
TEXT | 服务端 XML 原始元素名,按原样保存 |
field_value |
TEXT | 服务端原始文本值,允许空字符串 |
联合主键为 (customer_id, field_name)。每次更新一个用户时,先删除该用户的旧字段,再写入本次响应中的全部字段,确保被服务端删除的字段不会残留。外键级联删除。
3.3 同步记录表 customer_sync_runs
| 字段 | 类型 | 规则 |
|---|---|---|
sync_id |
TEXT | 主键,使用开始时间毫秒和进程内序号生成 |
started_at / completed_at |
INTEGER | 同步开始/结束毫秒值 |
expected_count |
INTEGER | GetCustomerCount 返回数量 |
saved_count |
INTEGER | 本轮累计成功保存数量 |
successful_batches |
INTEGER | 已提交的成功批次数 |
status |
TEXT | RUNNING、SUCCESS、FAILED |
error_message |
TEXT | 失败原因;成功时为空 |
4. 每批保存流程
- 创建同步记录并调用
GetCustomerCount。 - 使用当前分页常量请求一批
GetCustomerByNum。 - 从每个
PROXY_BASE_CUSTOMERS节点读取全部直接子元素,形成CustomerRecord(customerId, fields)。 - 如果某条记录缺少合法
CUSTOMERID、同一节点存在重复字段名、本批出现重复CUSTOMERID或本批没有任何用户,则整批解析失败,不写入数据库。 - 开启 SQLite 事务,对本批每个
CUSTOMERID执行主表 upsert、原始字段替换和last_seen_sync_id更新,同时更新同步进度。 - 事务提交后才开始下一批;提交失败时整批回滚,之前已提交批次不受影响。
- 下一批仍从当前批最大
CUSTOMERID + 1开始,剩余数量按实际保存条数递减。 - 全部批次成功后,在事务内删除
last_seen_sync_id != 当前 sync_id的旧用户,并把同步记录标记为SUCCESS。 - 任一后续请求、解析或保存失败时,把同步记录标记为
FAILED,保留之前成功批次,不执行旧用户清理。
5. 代码边界
CustomerRecord:不可变原始用户记录,持有唯一账号和有序原始字段。CustomerBatchParser:只负责安全解析PROXY_BASE_CUSTOMERS,不访问数据库。CustomerRepository:定义同步开始、批量保存、完成、失败和数量查询接口。SQLiteCustomerRepository:使用事务实现 SQLite 持久化和唯一性。CustomerSyncService:按 1000 条循环请求、逐批解析和保存,并通过回调报告进度。OnecardWebServiceClient:保留单个 SOAP 请求职责,不持有所有批次响应。ServerOperationsActivity:启动后台同步,页面只显示总数、当前批次、累计保存数、耗时和错误,不显示完整 XML。
6. 安全边界
由于数据库包含密码摘要和证件号:
- 数据库只使用
Context.getDatabasePath()管理的应用私有目录,不复制到外部存储。 - 原始字段和值不写入 Logcat、读卡器日志或测试报告。
- 页面不显示
PWD、QUERYPWD、IDCARDNO的实际值。 - Android Manifest 将
allowBackup改为false,防止敏感数据库进入系统自动备份。 - 本次使用系统 SQLite,不实现数据库加密;若设备安全要求提高,需要另立方案增加密钥管理和加密数据库。
7. 验证与验收
- JVM 测试已经验证现场观察到的51字段及未知字段解析、敏感字段保留、缺少账号和重复字段拒绝。
- 同步服务测试已经验证每批成功立即保存、后续失败保留前批、全部成功才清理旧数据。
- YF_029E 真机数据库3项测试已经验证主键唯一、字段替换、批事务回滚和同步完成/失败语义。
- YF_029E 本次相关数据库与服务器页面共7项真机测试通过。完整设备测试套件中的2项既有读卡器用例因测试时未识别到 HID 读卡器而失败,与本次服务器/SQLite 修改无关。
- 已完成 JVM 测试、Android 测试编译、Debug 构建和 Lint;最终执行结果以变更记录为准。
- 现场执行一次全部用户同步,核对服务器总数、成功保存数量、SQLite 唯一用户数和最终同步状态一致。
8. 非目标
- 本次不增加用户列表、详情、搜索或导出页面。
- 本次不保存整批 SOAP Envelope;保存的是每个用户节点的全部原始字段。
- 本次不实现增量接口、后台定时同步、自动重试、动态分页或数据库加密。