Commit 5f05a9272bdd8cc06c605858dc6360d976637b02

Authored by 权海
1 parent c2b507de

feat(ui):修复数据遗漏

... ... @@ -13,6 +13,7 @@
migrate_working_dir/
SDK/
ohos/
ohos/*
# IntelliJ related
*.iml
... ...
... ... @@ -2110,7 +2110,7 @@ class HealthRawStressLocalStore {
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 8,
version: 9,
onCreate: (db, version) async {
await _createTables(db);
},
... ... @@ -2137,6 +2137,9 @@ class HealthRawStressLocalStore {
if (oldVersion < 8) {
await _addUpdateTimeColumns(db);
}
if (oldVersion < 9) {
await _addDateKeyColumns(db);
}
},
),
);
... ... @@ -2148,6 +2151,7 @@ class HealthRawStressLocalStore {
await db.execute('''
CREATE TABLE IF NOT EXISTS $hrvResultsTable (
raw_end_time INTEGER PRIMARY KEY,
date_key INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL,
raw_hrv REAL NOT NULL DEFAULT 0,
result REAL NOT NULL,
... ... @@ -2169,6 +2173,7 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
await db.execute('''
CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
raw_end_time INTEGER PRIMARY KEY,
date_key INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL,
raw_hr REAL NOT NULL DEFAULT 0,
result REAL NOT NULL,
... ... @@ -2190,6 +2195,7 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
await db.execute('''
CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
date INTEGER PRIMARY KEY,
date_key INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL,
stress_value REAL NOT NULL,
stress_score INTEGER NOT NULL,
... ... @@ -2205,6 +2211,7 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
await db.execute('''
CREATE TABLE IF NOT EXISTS $sleepResultsTable (
date INTEGER PRIMARY KEY,
date_key INTEGER NOT NULL DEFAULT 0,
user_id INTEGER NOT NULL,
start_date INTEGER NOT NULL,
sleep_score INTEGER NOT NULL,
... ... @@ -2235,6 +2242,44 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
}
}
Future<void> _backfillDateKey(DatabaseExecutor db, String table) async {
final timeColumn = switch (table) {
hrvResultsTable || realtimeStressResultsTable => 'raw_end_time',
dailyStressResultsTable || sleepResultsTable => 'date',
_ => null,
};
if (timeColumn == null) return;
if (table == dailyStressResultsTable) {
await db.execute(
'UPDATE $table SET date_key = date WHERE date_key = 0 AND date > 0',
);
return;
}
await db.execute(
"UPDATE $table SET date_key = CAST(strftime('%Y%m%d', "
"$timeColumn, 'unixepoch', 'localtime') AS INTEGER) "
'WHERE date_key = 0 AND $timeColumn > 0',
);
}
Future<void> _addDateKeyColumns(DatabaseExecutor db) async {
for (final table in const [
hrvResultsTable,
realtimeStressResultsTable,
dailyStressResultsTable,
sleepResultsTable,
]) {
try {
await db.execute(
'ALTER TABLE $table ADD COLUMN date_key INTEGER NOT NULL DEFAULT 0',
);
} on DatabaseException catch (error) {
if (!error.isDuplicateColumnError()) rethrow;
}
await _backfillDateKey(db, table);
}
}
Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
for (final column in const [
'is_workout',
... ... @@ -2381,9 +2426,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
return now.year * 10000 + now.month * 100 + now.day;
}
int _dateKeyFromUnixSeconds(int seconds) {
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return date.year * 10000 + date.month * 100 + date.day;
}
Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
'user_id': point.userId,
'raw_hrv': point.rawHrv,
'result': _integerDouble(point.result),
... ... @@ -2403,6 +2454,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) {
return <String, Object?>{
'raw_end_time': point.rawEndTime,
'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
'user_id': point.userId,
'raw_hr': point.rawHr,
'result': _integerDouble(point.result),
... ... @@ -2417,6 +2469,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) {
return <String, Object?>{
'date': point.date,
'date_key': point.date,
'user_id': point.userId,
'stress_value': _integerDouble(point.stressValue),
'stress_score': point.stressScore,
... ... @@ -2430,6 +2483,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
Map<String, Object?> _sleepRow(HealthRawSleepResult result) {
return <String, Object?>{
'date': result.date,
'date_key': _dateKeyFromUnixSeconds(result.date),
'user_id': result.userId,
'start_date': result.startDate,
'sleep_score': result.sleepScore,
... ... @@ -2487,6 +2541,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
table,
<String, Object?>{
'user_id': row['user_id'],
'date_key': row['date_key'],
if (row.containsKey('raw_hrv')) 'raw_hrv': row['raw_hrv'],
if (row.containsKey('raw_hr')) 'raw_hr': row['raw_hr'],
'result': row['result'],
... ... @@ -2586,6 +2641,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
dailyStressResultsTable,
<String, Object?>{
'user_id': row['user_id'],
'date_key': row['date_key'],
'stress_value': row['stress_value'],
'stress_score': row['stress_score'],
'state': row['state'],
... ... @@ -2644,6 +2700,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
sleepResultsTable,
<String, Object?>{
'user_id': row['user_id'],
'date_key': row['date_key'],
'start_date': row['start_date'],
'sleep_score': row['sleep_score'],
'sleep_state': row['sleep_state'],
... ... @@ -2693,6 +2750,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
return <String>[
'user_id',
'date_key',
if (row.containsKey('raw_hrv')) 'raw_hrv',
if (row.containsKey('raw_hr')) 'raw_hr',
'result',
... ...
... ... @@ -281,8 +281,13 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
version: 2,
onCreate: (db, version) => _createTables(db),
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
await _addDateKeyColumns(db);
}
},
),
);
_opened[userId] = db;
... ... @@ -306,6 +311,7 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
data_type INTEGER,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
date_key INTEGER NOT NULL DEFAULT 0,
create_time INTEGER NOT NULL,
UNIQUE (from_time)
)
... ... @@ -342,6 +348,7 @@ CREATE TABLE IF NOT EXISTS $table (
time INTEGER NOT NULL,
value REAL,
is_asleep INTEGER,
date_key INTEGER NOT NULL DEFAULT 0,
create_time INTEGER NOT NULL,
UNIQUE (time)
)
... ... @@ -359,6 +366,7 @@ CREATE TABLE IF NOT EXISTS $table (
activity_type INTEGER,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
date_key INTEGER NOT NULL DEFAULT 0,
create_time INTEGER NOT NULL,
UNIQUE (from_time)
)
... ... @@ -372,10 +380,12 @@ CREATE TABLE IF NOT EXISTS $table (
OhosHealthRawDataItem item,
int createTime,
) {
final time = _numPayload(item.payload, 'time') ?? item.dataTime;
return {
'time': _numPayload(item.payload, 'time') ?? item.dataTime,
'time': time,
'value': _numPayload(item.payload, 'value'),
'is_asleep': _numPayload(item.payload, 'is_asleep'),
'date_key': _dateKeyFromUnixSeconds(time),
'create_time': createTime,
};
}
... ... @@ -384,14 +394,16 @@ CREATE TABLE IF NOT EXISTS $table (
OhosHealthRawDataItem item,
int createTime,
) {
final toTime = _numPayload(item.payload, 'to_time') ??
_numPayload(item.payload, 'end_time') ??
item.dataTime;
return {
'activity_type': _numPayload(item.payload, 'activity_type'),
'from_time': _numPayload(item.payload, 'from_time') ??
_numPayload(item.payload, 'start_time') ??
0,
'to_time': _numPayload(item.payload, 'to_time') ??
_numPayload(item.payload, 'end_time') ??
item.dataTime,
'to_time': toTime,
'date_key': _dateKeyFromUnixSeconds(toTime),
'create_time': createTime,
};
}
... ... @@ -400,10 +412,12 @@ CREATE TABLE IF NOT EXISTS $table (
OhosHealthRawDataItem item,
int createTime,
) {
final toTime = _numPayload(item.payload, 'to_time') ?? item.dataTime;
return {
'data_type': _numPayload(item.payload, 'data_type') ?? item.dataType,
'from_time': _numPayload(item.payload, 'from_time') ?? 0,
'to_time': _numPayload(item.payload, 'to_time') ?? item.dataTime,
'to_time': toTime,
'date_key': _dateKeyFromUnixSeconds(toTime),
'create_time': createTime,
};
}
... ... @@ -431,6 +445,7 @@ CREATE TABLE IF NOT EXISTS $table (
'time': dataTime,
'value': row['value'],
'is_asleep': row['is_asleep'],
'date_key': row['date_key'],
},
);
}
... ... @@ -446,6 +461,7 @@ CREATE TABLE IF NOT EXISTS $table (
'data_type': dataType,
'from_time': row['from_time'],
'to_time': toTime,
'date_key': row['date_key'],
},
);
}
... ... @@ -462,6 +478,7 @@ CREATE TABLE IF NOT EXISTS $table (
'activity_type': row['activity_type'],
'from_time': row['from_time'],
'to_time': toTime,
'date_key': row['date_key'],
},
);
}
... ... @@ -526,6 +543,61 @@ CREATE TABLE IF NOT EXISTS $table (
1000;
}
int _dateKeyFromUnixSeconds(int seconds) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
}
Future<void> _addDateKeyColumns(DatabaseExecutor db) async {
final rows = await db.query(
'sqlite_master',
columns: ['name'],
where: "type = 'table' AND (name = ? OR name LIKE ?)",
whereArgs: [sleepDataTable, '${rawDataTable}_%'],
);
for (final row in rows) {
final table = row['name'] as String?;
if (table == null) continue;
await _addColumnIfMissing(
db,
table,
'date_key INTEGER NOT NULL DEFAULT 0',
);
await _backfillDateKey(db, table);
}
}
Future<void> _backfillDateKey(DatabaseExecutor db, String table) async {
final columns = await db.rawQuery('PRAGMA table_info($table)');
final columnNames =
columns.map((row) => row['name']).whereType<String>().toSet();
final timeColumn = columnNames.contains('to_time') ? 'to_time' : 'time';
if (!columnNames.contains(timeColumn)) return;
await db.execute(
"UPDATE $table SET date_key = CAST(strftime('%Y%m%d', "
"$timeColumn, 'unixepoch', 'localtime') AS INTEGER) "
'WHERE date_key = 0 AND $timeColumn > 0',
);
}
Future<void> _addColumnIfMissing(
DatabaseExecutor db,
String table,
String columnSql,
) async {
try {
await db.execute('ALTER TABLE $table ADD COLUMN $columnSql');
} on DatabaseException catch (error) {
if (!_isDuplicateColumnError(error)) rethrow;
}
}
bool _isDuplicateColumnError(DatabaseException error) {
final message = error.toString().toLowerCase();
return message.contains('duplicate column') ||
message.contains('duplicate column name');
}
int? _numPayload(Map<String, Object?> payload, String key) {
final value = payload[key];
return value is num ? value.toInt() : null;
... ...
... ... @@ -293,7 +293,8 @@ class OhosHealthRawDataSyncService {
'chunkDays=$fetchChunkDays '
'startTime=${range.startTime} endTime=${range.endTime} '
'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '
'endDate=${_dateKeyFromUnixSeconds(range.endTime)}',
'endDate=${_dateKeyFromUnixSeconds(range.endTime)} '
'apiEndDate=${_exclusiveEndDateKeyFromUnixSeconds(range.endTime)}',
);
final page = await _remoteDataSource.fetchRawDataPage(
... ... @@ -501,10 +502,9 @@ class OhosHarmonyHealthRawDataRemoteDataSource
OhosHarmonyHealthRawDataRemoteDataSource(
this._client, {
DateTime Function()? nowProvider,
}) : _nowProvider = nowProvider ?? DateTime.now;
});
final OhosHarmonyRawDataClient _client;
final DateTime Function() _nowProvider;
@override
Future<OhosHealthRawDataPage> fetchRawDataPage({
... ... @@ -513,7 +513,7 @@ class OhosHarmonyHealthRawDataRemoteDataSource
required int endTime,
}) async {
final startDate = _dateKeyFromUnixSeconds(startTime);
final endDate = _endDateKeyFromUnixSeconds(endTime);
final endDate = _exclusiveEndDateKeyFromUnixSeconds(endTime);
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
final result = await _client.getSleepData(startDate, endDate);
return switch (result) {
... ... @@ -630,17 +630,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource
)
.toList(growable: false);
}
int _endDateKeyFromUnixSeconds(int seconds) {
final end = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
final now = _nowProvider();
final today = DateTime(now.year, now.month, now.day);
final endDate = DateTime(end.year, end.month, end.day);
if (endDate == today) {
return _dateKeyFromDateTime(today.add(const Duration(days: 1)));
}
return _dateKeyFromDateTime(endDate);
}
}
class TodoOhosHealthRawDataRemoteDataSource
... ... @@ -830,6 +819,12 @@ int _dateKeyFromUnixSeconds(int seconds) {
return _dateKeyFromDateTime(dateTime);
}
int _exclusiveEndDateKeyFromUnixSeconds(int seconds) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
final endDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
return _dateKeyFromDateTime(endDate.add(const Duration(days: 1)));
}
int _dateKeyFromDateTime(DateTime dateTime) {
return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
}
... ...
... ... @@ -106,6 +106,9 @@ void main() {
expect(result.segmentCount, remote.calls.length);
expect(remote.calls.first.startTime, expectedStart);
expect(remote.calls.last.endTime, expectedEnd);
for (var i = 1; i < remote.calls.length; i += 1) {
expect(remote.calls[i].startTime, remote.calls[i - 1].endTime + 1);
}
expect(local.storedBatches, isEmpty);
});
... ... @@ -480,13 +483,28 @@ void main() {
expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv);
expect(client.healthCalls.single.startDate, 20260801);
expect(client.healthCalls.single.endDate, 20260831);
expect(client.healthCalls.single.endDate, 20260901);
expect(page.items, hasLength(1));
expect(page.items.single.dataType, 1);
expect(page.items.single.dataTime, 1787900000);
expect(page.items.single.payload['value'], 53);
});
test('Harmony remote data source uses exclusive endDate for historical day',
() async {
final client = _FakeOhosHarmonyRawDataClient();
final remote = OhosHarmonyHealthRawDataRemoteDataSource(client);
await remote.fetchRawDataPage(
dataType: HuaweiHealthDataType.hrv.dataType,
startTime: _unixSeconds(DateTime(2026, 8, 30)),
endTime: _unixSeconds(DateTime(2026, 8, 30, 23, 59, 59)),
);
expect(client.healthCalls.single.startDate, 20260830);
expect(client.healthCalls.single.endDate, 20260831);
});
test('Harmony remote data source uses tomorrow as endDate for today data',
() async {
final client = _FakeOhosHarmonyRawDataClient();
... ... @@ -530,7 +548,7 @@ void main() {
);
expect(client.sleepCalls.single.startDate, 20260801);
expect(client.sleepCalls.single.endDate, 20260802);
expect(client.sleepCalls.single.endDate, 20260803);
expect(page.items, hasLength(1));
expect(page.items.single.dataType, 4);
expect(page.items.single.dataTime, 1787870000);
... ...