☰
OpenHarmony下Flutter电子合同App的数据持久化实践
2026/10/10 6:32:44 网站建设 项目流程

最近一个项目需要用Flutter for OpenHarmony做电子合同签署App,业务跑起来之后,我发现最折磨人的反而不是页面动画或者状态管理,而是数据持久化。电子合同App和普通工具类App不一样,它既有合同元数据、签署记录、身份信息这类结构化数据,又有合同PDF、签名图片这类大文件,还要考虑敏感字段加密、签署流程的事务一致性。在OpenHarmony这个相对年轻的生态里,Flutter相关的持久化插件还不像Android那么成熟,很多“老经验”都得重新验证。这篇文章就把我在这套方案里的完整实现思路、建表逻辑、代码细节和踩坑记录整理出来,给准备做同类型App的人一个能直接参考的落地方案。

1. 从业务场景出发的持久化方案选型

1.1 电子合同签署App里到底有哪些数据要存

在设计持久化方案前,我先把App里的数据实体全部梳理了一遍。这个步骤看起来基础,但直接决定后面建表和存储分层的合理性,不能跳过去。

合同签署App的持久化数据可以分成四类:

  • 合同元数据:合同编号、标题、类型、金额、签约双方主体、合同状态、创建时间、更新时间、本地文件路径。
  • 签署记录:合同ID、签署人姓名、签署人证件号码、签署方式(手写签名/印章/验证码)、签名图片路径、签署时间、签署流程状态(待签、已签、已完成、已作废)。
  • 附件索引:PDF合同文件、签名图片、可能的身份证照片扫描件,对应业务类型和业务ID。
  • 本地配置:签名样式偏好、最近使用的文件夹位置、登录令牌、协议版本号等轻量级数据。

这里面有一个容易被忽略的点:合同正文里的条款内容、甲方乙方姓名、证件号码这些属于高度敏感信息,不能和设备上的普通文件一样裸存。所以我把“合同内容”单独拆出来做字段级加密,而不是整份文件直接扔到磁盘上。这个决策在后面建表和存储时会体现出来。

从访问频率和更新方式看,合同元数据和签署记录是高频查询对象,需要支持按状态筛选、按合同ID关联查询,适合用关系型数据库。签名图片和PDF文件体积大、读取频繁但数据库不关心文件内部结构,适合用文件系统存储,数据库只保存路径和校验值。登录令牌和用户偏好是简单的键值对,用轻量级存储就够了。

1.2 OpenHarmony环境下持久化选型为什么不能照搬

做过Flutter跨端开发的人都知道,Android上用的squirrel、SQLite插件在OpenHarmony上并不一定生效。因为OpenHarmony的原生层接口和Android是不同的,自带的SQLite实现也不一定能直接被Dart层调用,必须找到为OpenHarmony适配过的插件。

我在项目启动时先调查了可用的存储能力。OpenHarmony 4.0以上的系统本身提供分布式数据管理服务,包括关系型数据库、键值数据库和偏好存储。但是对于Flutter应用,绕开平台层直接用这些能力难度很大,更务实的路线是找社区里已经适配OpenHarmony的Flutter插件。需要注意,OpenHarmony的适配插件往往是以_ohos结尾的版本,依赖的SDK版本、API级别和Android版本不同,不能混用。

当时我对比了三条路:

  1. 使用OpenHarmony原生关系型数据库插件,再通过MethodChannel封装给Dart使用。这个方案最灵活,但需要自己写大量平台代码,后续升级维护成本高,不适合赶业务进度。
  2. 使用社区提供的SQLite适配库。这个方案对业务开发最友好,因为API基本沿用Android生态下的使用习惯,包一层之后团队上手很快。
  3. 使用纯Dart文件方案,把数据序列化成JSON或CSV文件保存。只适合配置量级,做不了复杂查询和事务,会在签署流程中迅速失控。

最终我选了方案二,底层用适配过的SQLite插件,配合OpenHarmony适配的路径获取库和偏好存储库。整体方案在项目里稳住了,做到了新增、修改、查询、事务回滚、加密和备份都围绕同一套存储抽象,后面每一部分我都会展开讲。

2. 环境搭建与依赖适配:Flutter for OpenHarmony 的持久化基础

2.1 依赖引入与初始化坑位

在Flutter工程里引入持久化依赖,最核心的是保证版本对齐。我当时在pubspec.yaml里添加了下面这几个包:

dependencies: flutter: sdk: flutter sqflite_ohos: ^2.4.0 path_provider_ohos: ^2.2.0 shared_preferences_ohos: ^2.2.0 cryptography: ^2.5.0

注意这里我写的是示例版本,实际版本号需要根据你的OpenHarmony SDK版本来定。如果你是从Android转移过来的工程,不要直接用原来的sqflite包,必须换成sqflite_ohos。同理,path_provider_ohos负责获取可用的目录路径,shared_preferences_ohos负责键值存储。社区里还有sembast_ohos、hive_ohos等选择,但我建议项目里尽量少引入数据库种类,集中维护一套,否则升级适配会非常痛苦。

初始化时最容易踩坑的是路径获取。在Android上getDatabasesPath()通常返回/data/data/包名/databases/,而在OpenHarmony环境里,如果没有正确调用适配插件,这个接口可能返回空字符串或者抛出MissingPluginException。我的做法是在应用启动时做一个存储环境自检,把数据库路径和文档目录先打印出来,确认目录存在再往下走。

final dbDir = await getDatabasesPath(); final docDir = await getApplicationDocumentsDirectory(); debugPrint('database dir: $dbDir'); debugPrint('document dir: $docDir');

我遇到过一个问题:某些低版本OpenHarmony设备的getDatabasesPath()返回路径的父目录不存在,导致后续openDatabase失败。解决办法是主动创建目录,同时做好目录存在性判断,避免重复创建报错。

2.2 数据分层:配置、结构化数据、文件分开管

我的持久化架构不是一把梭把所有数据都塞进SQLite,而是按数据特性和访问频率做了三层划分:

数据层级存储载体适用数据主要编程接口
轻量配置层SharedPreferences登录态、签名偏好、协议版本SharedPreferences.getInstance()
结构化业务层SQLite数据库合同元数据、签署记录、附件索引Database对象、insert/query/update/transaction
文件层应用私有目录PDF合同、签名图片、缓存文件File、RandomAccessFile、sha256
加密层密钥库 + 加密算法身份证号、合同正文敏感字段cryptography包 + 设备端密钥存储

分层的好处很直接。合同列表页只需要查数据库,不需要读取PDF文件,查询性能不会受文件大小影响。只有签署预览时才通过数据库里的路径去加载PDF,文件层再独立做缓存和清理。配置数据则完全不走数据库,避免每次读写都打开数据库连接,省掉不必要的开销。

这里我特别说明一下为什么没有把加密层单独做成一个“存储方案”,而是作为拦截器嵌在数据库读写之间。因为加密通常是针对具体字段的,比如身份证号需要加密,合同编号却需要明文用于查询。如果把整个数据库加密,反而无法用SQL的WHERE contract_id = ?高效定位记录。所以我在模型层做了字段级加密封装,对外只暴露明文,内部负责加解密。这样业务代码不需要关心加密细节,持久化层也保持了简单。

3. 数据库设计与建表实操:给签署流程打地基

3.1 三张核心表的结构设计与索引规划

SQLite的本质是帮我们管理稳定可靠的关系数据。我设计了contract、sign_record、attachment三张表,分别对应合同主档、签署记录和附件索引。

contract表结构:

CREATE TABLE contract ( id TEXT PRIMARY KEY, title TEXT NOT NULL, contract_no TEXT NOT NULL UNIQUE, party_a TEXT NOT NULL, party_b TEXT NOT NULL, amount REAL, status INTEGER NOT NULL DEFAULT 0, body_encrypted TEXT, local_pdf_path TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL );

主键用text类型的合同ID而不是自增ID,是因为合同编号会同步到其他系统,用UUID或业务规则生成更稳定;同时防止跨设备恢复数据时自增ID冲突。status用整数表示,方便扩展和比较,0表示草稿、1表示待签署、2表示签署中、3表示已完成、4表示已作废。body_encrypted字段存合同正文加密后的密文,local_pdf_path保存PDF文件路径。

sign_record表结构:

CREATE TABLE sign_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, contract_id TEXT NOT NULL, signer_name TEXT NOT NULL, signer_id_card_encrypted TEXT NOT NULL, sign_type INTEGER NOT NULL, sign_image_path TEXT, sign_time INTEGER NOT NULL, flow_status INTEGER NOT NULL, unique_hash TEXT NOT NULL UNIQUE, FOREIGN KEY (contract_id) REFERENCES contract(id) );

signer_id_card_encrypted存放加密后的证件号码,这里再次体现字段级加密的价值。unique_hash用于防重,同一份合同同一个签署人只能签一次,数据库唯一索引兜底,比应用层判断更可靠。

attachment表结构:

CREATE TABLE attachment ( id INTEGER PRIMARY KEY AUTOINCREMENT, biz_type TEXT NOT NULL, biz_id TEXT NOT NULL, file_path TEXT NOT NULL, mime_type TEXT NOT NULL, file_size INTEGER NOT NULL, file_hash TEXT NOT NULL, created_at INTEGER NOT NULL );

附件表的作用是建立一个“文件索引中心”。PDF和图片都以文件形式保存在私有目录,附件表保存路径和校验信息。这样清理目录、验证文件完整性、统计存储容量都有据可查。

为了查询性能,我给表建了索引:

CREATE INDEX idx_contract_status ON contract(status); CREATE INDEX idx_sign_record_contract ON sign_record(contract_id, sign_time); CREATE INDEX idx_attachment_biz ON attachment(biz_type, biz_id);

一开始我偷懒没建contract(status)索引,结果合同列表在数据量过万后翻页明显变慢,加上索引之后查询时间从几百毫秒降到几十毫秒。所以测试时一定用大数据量验一遍SQL执行计划,别只在十几条数据上自嗨。

3.2 数据库版本升级与连接管理

数据库打开逻辑我封装成了一个AppDatabase单例,让整个App只有一份数据库连接,避免连接泄漏。这里给出核心代码:

class AppDatabase { static const _dbName = 'contract_signer.db'; static const _dbVersion = 2; AppDatabase._(); static final AppDatabase instance = AppDatabase._(); static Database? _database; Future<Database> get database async { _database ??= await _openDatabase(); return _database!; } Future<Database> _openDatabase() async { final dbPath = await getDatabasesPath(); final path = '$dbPath/$_dbName'; final db = await openDatabase( path, version: _dbVersion, onConfigure: (db) async { await db.execute('PRAGMA foreign_keys = ON'); }, onCreate: (db, version) async { await db.execute(_createContractTable); await db.execute(_createSignRecordTable); await db.execute(_createAttachmentTable); await db.execute(_createIndexes); }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 2) { await db.execute('ALTER TABLE contract ADD COLUMN body_encrypted TEXT'); } }, ); await db.execute('PRAGMA journal_mode = WAL'); return db; } }

onConfigure里开启外键约束,这是为了让OracleContractId存在性校验下沉到数据库层,防止脏数据写入。注意不要在onConfigure里执行PRAGMA journal_mode = WAL,虽然能执行,但部分版本的SQLite不允许在事务中修改journal mode,有可能报错。我是在openDatabase成功之后单独执行这个pragma,执行一次即可,它会持久化到数据库文件中。

版本升级这块,一定要设计好迁移策略。项目上线后如果改了表结构,onUpgrade里不能只做ADD COLUMN,还要考虑旧数据和新字段的关系。比如我给contract表加body_encrypted字段时,先判断列是否存在,避免重复添加。SQLite的ALTER TABLE ADD COLUMN能增加一个可空列,但没法往已有行里填默认值,所以升级后要跑一个补数据脚本,把历史合同正文迁移加密后写入新字段。这个脚本需要放在main()里异步执行,确保不阻塞首页渲染。

4. 核心持久化流程实现与代码拆解

4.1 用事务保证签署流程的原子性

电子合同签署是一个典型的多步骤写操作。一次签署动作至少包括三件事:合同状态更新为“已完成”、插入一条签署记录、在附件表里登记签名图片路径。如果其中某一步成功、某一步失败,就会留下状态错乱的数据,最典型的是合同显示“已完成”但没有签署记录,这在合同场景里是不可接受的。因此必须用SQLite事务包裹。

实现代码示意:

Future<bool> submitSign({ required String contractId, required SignRecord record, }) async { final db = await AppDatabase.instance.database; try { await db.transaction((txn) async { final updateCount = await txn.update( 'contract', {'status': 3, 'updated_at': DateTime.now().millisecondsSinceEpoch}, where: 'id = ? AND status = ?', whereArgs: [contractId, 2], ); if (updateCount == 0) { throw Exception('当前合同状态不允许签约'); } await txn.insert('sign_record', record.toDbMap()); await txn.insert('attachment', { 'biz_type': 'sign_image', 'biz_id': contractId, 'file_path': record.signImagePath, 'mime_type': 'image/png', 'file_size': await File(record.signImagePath).length(), 'file_hash': await _sha256File(record.signImagePath), 'created_at': DateTime.now().millisecondsSinceEpoch, }); }); return true; } catch (e) { debugPrint('submitSign failed: $e'); return false; } }

事务代码中有几个容易被忽视的细节:

第一,更新语句必须带状态条件。我用status = ?的条件来防止重复提交。如果两个请求同时进来,只有第一个能更新成功,第二个会拿到updateCount = 0并抛异常回滚。这是比应用层加锁更稳妥的方式,数据库行锁天然保证并发安全。

第二,事务过程中如果抛异常,SQLite会自动回滚transaction之前的所有修改。但要注意,如果这是嵌套事务或者你在事务里又去操作文件系统,文件并不会自动回滚。所以文件的写入应该放在事务之前或之后,并单独处理失败补偿。我在实际项目里把“签名图片写入磁盘”放在事务之前,事务失败后主动删除已经写入的图片文件,保证文件和DB记录一致。

第三,事务内的查询也要用txn而不是外层db。如果你用外层db.query去查询事务中没有提交的数据,某些隔离级别下会看不到,导致逻辑判断错误。我在早期开发中踩过这个坑,看起来一个小小的对象引用问题,排查了很久。

4.2 敏感字段加密与密钥安全

电子合同中身份证号、公司信息属于个人敏感数据,不能明文落库。我在模型层封装了一个CryptoService,业务代码只需要调用encryptField和decryptField就能完成加解密。

加密方案我选择了AES-GCM。AES-GCM是认证加密,能检测密文在存储或传输过程中是否被篡改,比单独的AES-CBC更安全。密钥不能写死在代码里,也不能只存在Dart层,需要交给系统级密钥管理能力。OpenHarmony本身提供了密钥管理服务,在纯Flutter工程里通过内部渠道获取起来比较绕,项目里我们是用了一个适配OpenHarmony的密钥存储封装,把密钥保存在设备安全区域中,业务层只能拿到使用句柄,拿不到原始密钥值。

下面是加解密核心示意代码:

class CryptoService { final _algorithm = AesGcm.with256bits(); SecretKey? _key; Future<SecretKey> _getOrCreateKey() async { final provider = await _getSecureKeyStorage(); if (await provider.contains('app_main_key')) { final raw = await provider.read('app_main_key'); return SecretKey(raw); } final key = await _algorithm.newKey(); await provider.write('app_main_key', await key.extractBytes()); return key; } Future<String> encryptField(String plaintext) async { final key = await _getOrCreateKey(); final secretBox = await _algorithm.encryptString(plaintext, secretKey: key); return '${secretBox.nonce.base64}:${secretBox.cipherText.base64}:${secretBox.mac.base64}'; } Future<String> decryptField(String stored) async { final parts = stored.split(':'); final secretBox = SecretBox( await base64Decode(parts[1]), nonce: await base64Decode(parts[0]), mac: Mac(await base64Decode(parts[2])), ); final key = await _getOrCreateKey(); return _algorithm.decryptString(secretBox, secretKey: key); } }

需要提一下,加密后字段长度会变长,字段加密后以Base64字符串形式存储,所以表设计时相关字段给了TEXT类型,而不是固定长度VARCHAR。另外,加密字段无法直接做模糊查询。比如想通过身份证号搜索合同,就不能LIKE '%加密前内容%'。我的做法是:当业务上需要按证件号定位记录时,额外存一列证件号的散列值,用SHA-256计算后做等值查询。散列值不可逆,又支持索引匹配,兼顾安全和查询需求。当然如果有人用字典碰撞攻击,安全性取决于证件号本身的复杂度,我在推行中还会在散列时加盐,进一步抵抗彩虹表。

密钥备份是另一个重要问题。设备密钥存在安全区后,跨设备迁移或应用重装会导致密钥丢失,所有密文都无法解密。我在方案里做了两步处理:一是给用户提供“导出密文签名包”功能,允许把密文和加密密钥的副本用用户口令包裹后备份;二是设计密钥标识字段,每个合同记录保存密钥版本号。将来升级密钥轮换时,先读取旧版本密钥解老数据,再加密写入新版本密钥,保证平滑迁移。这块逻辑虽然多一点,但在合同类应用里属于必须做的合规功课。

4.3 合同PDF与签名图片的文件持久化细节

数据库只负责元数据,PDF和图片需要落到文件系统。我在文件层做了几个关键约束:

第一,所有文件都保存在应用私有目录下。通过getApplicationDocumentsDirectory()拿到基础路径,再按业务类型创建子目录。合同PDF放在documents/contract_files/,签名图片放在documents/sign_images/。使用私有目录的好处是不需要申请存储权限,也避免用户通过相册或文件管理器不小心改动合同文件,保证完整性。

第二,文件名规划要包含足够信息,但不能太长。我使用的命名规则是{bizId}_{timestamp}_{random}.pdf,签名图片为{contractId}_sign_{signerIdHash}_{timestamp}.png。这样可以避免重复,同时方便事后人工核对。不要在文件名里直接放身份证号,隐私泄露风险太高。

第三,写文件使用流式写入,避免一次性把大文件读入内存。某些合同PDF可能达到几十MB,直接await File(path).writeAsBytes(bytes)可能会让UI卡顿,甚至触发内存警告。更好的写法是:

Future<void> writeContractFile(String path, Stream<List<int>> chunkStream) async { final file = File(path); await file.create(recursive: true); final sink = file.openWrite(); await for (final chunk in chunkStream) { sink.add(chunk); } await sink.close(); }

读取PDF预览时也不要用readAsBytes一次性加载到内存。使用FileImage或自定义流读取器分段加载,分页渲染,避免大文件导致OOM。

第四,写入后必须计算文件哈希并写入附件表。我在前面的事务代码里已经演示了SHA-256的使用。文件哈希有两个作用:一是校验文件损坏,对比哈希能在列表页提醒用户“该合同文件已损坏”;二是签名防伪存证,与签署记录绑定后可以证明这份文件没有被替换过。文件哈希计算的实现如下:

Future<String> _sha256File(String path) async { final file = File(path); final digest = await file.openRead().fold(sha256, (h, d) => h.update(d)); return digest.close(); }

实际项目中,我还会定期把附件表的file_hash和磁盘实际文件哈希做对比,作为巡检任务每天晚上跑一次。如果发现不一致,自动把对应合同标记为“数据异常”,并阻止继续签署。这个机制虽然小,但在正式环境里能避免不少纠纷。

5. 常见问题、性能优化与一致性保障

5.1 数据库并发写冲突的排查与规避

Flutter的异步模型容易让开发者误以为所有数据库操作都是串行的。事实上,如果同一个Database实例被多个页面的Future并发使用,并且各自开启了事务,就可能出现“database is locked”或“cannot start a transaction within a transaction”错误。

我在这类项目中总结了一套规避方案:

  1. 数据库连接单例化,避免每个函数都调用openDatabase创建一个新连接。
  2. 对需要严格串行化的写操作增加一个Future队列。最简单的实现可以用一个全局的Future链来串行执行事务,避免互相穿插。
class SyncQueue { Future<void> _tail = Future.value(); Future<T> run<T>(Future<T> Function() action) { final result = _tail.then((_) => action()); _tail = result.then((_) {}, onError: (_) {}); return result; } }

在签署操作里,我用SyncQueue包装submitSign,确保同一时间只有一个签署事务在跑。虽然SQLite本身有锁机制,但在事务中做多个操作时,锁等待可能会让用户体验变差,串行化能有效避免“数据库已锁定”的弹窗。

还有一个细节:查询操作尽量走索引,避免全表扫描导致长时间占用数据库锁。特别是sign_record表会不断增长,如果每次查询都扫描整表,数据量大了之后不仅慢,还会加剧锁竞争。因此前面建立的联合索引(contract_id, sign_time)非常重要。

5.2 文件与数据库不一致的恢复策略

文件系统和数据库是两个独立系统,任何一步都可能崩溃,导致两者不一致。最常见的场景是:我把PDF文件写入了磁盘,但数据库还没写入记录时App被强杀,留下一个“孤儿文件”;或者数据库已经写上记录但文件还没写完,重启后查询到一条指向不存在文件的记录。

我的修复方案分为两层。

在写入侧,采用“两步提交”思路:先写临时文件,完成后再rename为正式文件名。由于文件重命名在同一个文件系统里是原子操作,不会出现半截文件被当成正式文件的情况。接着写数据库,如果数据库写失败,删除已经rename的文件。

在恢复侧,App启动时执行一个一致性检查任务,遍历ATTACHMENT表,逐个检查file_path对应的文件是否存在。如果文件缺失但数据库记录存在,把记录标记为STATUS_MISSING,同时在UI展示修复入口。如果文件存在但数据库无记录,则计算文件哈希并扫描文件名中的业务标识,尝试找回关联合同;无法关联的目录文件在保留7天后清理。

这段检查逻辑放在启动后的低优先级异步任务中,不阻塞主流程,但能有效避免隐藏数据问题在关键时刻爆发。下面给出核心伪代码:

Future<void> repairInconsistency(Database db) async { final rows = await db.query('attachment'); for (final row in rows) { final file = File(row['file_path'] as String); if (!await file.exists()) { await db.update( 'attachment', {'status': 1}, where: 'id = ?', whereArgs: [row['id']], ); } } }

5.3 批量场景下的性能优化

电子合同App往往需要支持批量导入合同。批量写入上万条数据时,如果逐条执行db.insert,耗时可能达到分钟级别。SQLite的Batch接口能把多条SQL拼接成一次提交事务,性能提升非常明显。

Future<void> batchInsertContracts(List<Contract> list) async { final db = await AppDatabase.instance.database; final batch = db.batch(); for (final c in list) { batch.insert('contract', c.toDbMap()); } await batch.commit(noResult: true); }

另外,我把PRAGMA journal_mode = WAL开启后,读操作和写操作能够并行执行,不再互相阻塞。这份收益在“合同列表滚动 + 后台批量导入”的场景下感触很深。开启WAL后,CREATE INDEX等操作也会更顺畅,但要注意开启后数据库文件旁边会多出-wal和-shm文件,备份时必须把这几个文件一并读取,不能只拷贝主库文件。

批量查询需要注意分页方案。列表页不应该一次把所有合同查出来,我采用的是LIMIT+OFFSET+总条数缓存的分页方案。合同状态筛选和分页组合起来时,要确保索引顺序匹配,否则OFFSET大了之后查询越来越慢。更高效的方案是“游标分页”,用WHERE id > lastId替代OFFSET,但需要UI配合,我是在数据量稳定后逐步切换的。

5.4 真机运行中的平台适配问题速查

在OpenHarmony设备上调试持久化功能,我还遇到了几类典型的适配问题,列出来供排查参考:

问题现象可能原因解决方法
MissingPluginException使用了Android版插件或未启用OpenHarmony注册替换为_ohos后缀插件,重编调试产物
getDatabasesPath()返回空path_provider适配失败确认path_provider_ohos版本,初始化后打印查看
数据库文件生成后重启读不到路径写成绝对路径且未创建目录主动创建目录,或使用插件默认路径
写入文件后图片显示空白文件写入完成前访问,路径包含非法字符使用流式写入,并对文件名做合法性校验
合同PDF打不开文件哈希不匹配或文件损坏使用文件哈希校验,并检查下载异常

这里特别提一下,不要在主线程同步执行File(path).readAsBytes()。电子合同PDF较大时会造成明显的页面卡顿,甚至卡死。我后来在compute隔离线程中做文件的哈希和尺寸读取,UI体验好了很多。Flutter的compute只能传递简单数据,不能传大对象,所以我传的是路径字符串,在隔离线程里重新创建对象进行操作。记得在操作完成后返回需要的字段。

6. 写在最后

我在这套Flutter for OpenHarmony电子合同签署App的数据持久化实现上,前后试过裸SQLite、整库加密、纯文件存储等多套方案,最后才稳定成“SQLite + 应用私有目录 + 字段加密 + 文件索引”的组合。最深的体会是:持久化不是把数据写进去就完事,而是一套围绕一致性、安全性、性能的工程问题。无论是事务包裹签署流程,还是文件与数据库的一致性恢复,都是在真实业务压力下被逼出来的。

对于准备自己动手实现同类型App的开发者,我的建议是先花一天时间把数据实体和访问模式梳理清楚,再决定存储分层。千万不要一开始就把所有压力压给数据库,也不要为了“简单”把所有内容扔到JSON文件里。希望这篇文章里的建表语句和代码片段能帮你少踩几个坑。后面如果有机会,我还会把“电子合同存证签名链”和“多端数据同步”的实现再整理出来继续分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询