如果你也玩三国杀,应该能理解那种打完一局精彩对局后,想赶紧把战绩记下来的冲动。武将、身份、胜负、击杀数——这些数据攒得越多,越能看出自己在哪个身份、哪个武将上容易出彩。问题在于,我用的是OpenHarmony系统的设备,应用商店里翻来覆去就那么几个工具,能打的攻略类应用几乎没有,更别提带战绩管理的了。后来我想,既然Flutter社区一直在做OpenHarmony适配,干脆自己动手写一个,把三国杀攻略App做成,顺便把战绩记录功能从数据层到界面完整趟一遍。
这篇文章就围绕我在Flutter for OpenHarmony上做三国杀攻略App的开源实践展开,核心是里面的战绩记录模块。我会把数据建模、数据库选型、页面实现、真机调试这几个关键环节遇到的问题和取舍都摆出来,适合两类人看:一类是想在OpenHarmony上做Flutter应用,但不确定插件生态靠不靠谱的开发者;另一类是三国杀玩家,想给自己做一个纯粹的本地战绩工具。代码思路不复杂,但每一步踩过的坑和选型的理由很值得聊一聊。
1. 立项背景:OpenHarmony上为什么缺一款这样的App
1.1 三国杀玩家的战绩记录需求
三国杀这个游戏的胜负信息质量其实很高。一场身份局玩下来,至少包含了这几个关键维度:游戏模式(标准、军争、国战)、身份(主公、忠臣、反贼、内奸)、你使用的武将、最终是否胜利、击杀数、造成伤害量,以及这局游戏的时长。这些数据如果只是打完就忘,等于白玩——因为你没法复盘自己到底是哪个武将胜率高,哪个身份胜率惨淡。
市面上虽然有一些三国杀助手类的应用,但大多绑定在Android和iOS生态里,在OpenHarmony设备上很难找到原生可用的版本。而且这些App往往做得过于臃肿,各种推送和活动弹窗反而干扰了核心的那件事:记录和复盘自己的战绩。我自己是那种打完一局好局必须马上记下来的人,所以一个干净、离线优先、可以随时翻阅统计数据的本地战绩工具,对我来说就是刚需。
1.2 Flutter for OpenHarmony的适配现状
说到跨平台方案,Flutter在OpenHarmony上的适配,一直是社区在推进的事。目前OpenHarmony SIG组维护了ohos分支的Flutter SDK,核心的Dart运行时和渲染引擎已经能跑起来,基础的Widget也没问题。但相比Android和iOS,插件生态还在补课。
我实际用下来,状态管理(Provider、Riverpod)、图表库(fl_chart)、网络库(dio)这些纯Dart插件可以直接用,因为不涉及原生代码。但需要调用平台能力的插件就麻烦了,比如数据库、文件路径、包信息,这些必须有对应的OHOS实现。社区里已经有sqflite_ohos、path_provider_ohos、shared_preferences_ohos这类适配包,覆盖了日常开发的大部分场景,但版本维护和文档完整度还是比成熟平台差一些。
所以在项目启动之前,我先给自己定了一条铁律:能选纯Dart实现就选纯Dart实现,必须走原生能力的,优先找ohos适配过的社区包,找不到就先绕过,比如用临时方案顶替。这条规则在后面让我少踩了很多坑。
1.3 为什么攻略App是练手OHOS的最佳项目
很多人听到OpenHarmony上跑Flutter,第一反应是做一个Hello World或者简单的Todo List,但我觉得这种项目信息量太少,跑通了也不能证明什么。三国杀攻略App不一样:它有数据结构设计、有本地持久化、有列表展示、有统计图表、有状态管理,几乎把日常App开发会遇到的核心问题都覆盖了一遍。
更关键的是,它的功能边界很清晰。我不需要做游戏本身的实时对战逻辑,也不涉及太多复杂的网络同步,核心就是"录入一条战绩、存起来、统计出来"这个闭环。这个闭环在Android上做一遍和在OpenHarmony上做一遍,差异点全在平台适配层,非常适合验证Flutter跨端能力在OHOS上到底可用到什么程度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 战绩系统的数据设计:先从一局游戏说起
2.1 一局三国杀战绩到底要记录哪些字段
设计数据表之前,我习惯先想"一条完整的战绩记录,用户在录入时看到什么、看完之后想查什么"。完整模拟用户的使用场景:晚上打了一局军争身份局,我选了孙权做主公,最后赢了,杀了两个反贼,打了三回合结束。这样一条记录,至少要能回答这些问题:什么时候打的、打的什么模式、我是什么身份、用的什么武将、赢了没有、我做了什么贡献。
按照这个思路,我定义了一张battle_records表,字段如下表所示:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER 自增主键 | 记录唯一标识 |
| general | TEXT | 使用的武将名 |
| identity | INTEGER | 身份编码,0主、1忠、2反、3内 |
| mode | TEXT | 游戏模式,标准/军争/国战/斗地主等 |
| is_win | INTEGER | 是否胜利,0失败、1胜利 |
| kill_count | INTEGER | 击杀数,默认0 |
| damage | INTEGER | 造成伤害量,默认0 |
| duration_seconds | INTEGER | 对局时长,单位秒 |
| battle_time | INTEGER | 对局发生的Unix时间戳(毫秒) |
| note | TEXT | 备注,可记录队友ID、精彩瞬间等 |
| create_time | INTEGER | 记录创建时间,Unix时间戳(毫秒) |
有两个在设计时容易忽略的点,我想提醒一下。
第一,identity字段我用整数编码而不是直接存字符串。原因不是省空间,而是后面的统计查询会轻松很多。按身份分组统计胜率时,GROUP BY identity比GROUP BY identity_text要直观,而且如果以后支持多语言,编码不会跟着界面文字走。
第二,battle_time和create_time分开存。battle_time是玩家手动选择的实际对局时间,create_time是这条记录真正写入数据库的时间。这两个时间在用户补录历史战绩时会明显不一致,如果只存一个字段,后续按时间排序和统计就会出现偏差。
2.2 数据实体与SQL建表语句
在Flutter端,我建了一个对应的实体类BattleRecord,字段和表结构一一对应。用Dart写出来大概是这样:
dart复制class BattleRecord {
final int? id;
final String general;
final int identity;
final String mode;
final bool isWin;
final int killCount;
final int damage;
final int durationSeconds;
final int battleTime;
final String note;
final int createTime;
const BattleRecord({
this.id,
required this.general,
required this.identity,
required this.mode,
required this.isWin,
required this.killCount,
required this.damage,
required this.durationSeconds,
required this.battleTime,
required th
