Commit 5f05a9272bdd8cc06c605858dc6360d976637b02

Authored by 权海
1 parent c2b507de

feat(ui):修复数据遗漏

@@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
13 migrate_working_dir/ 13 migrate_working_dir/
14 SDK/ 14 SDK/
15 ohos/ 15 ohos/
  16 +ohos/*
16 17
17 # IntelliJ related 18 # IntelliJ related
18 *.iml 19 *.iml
@@ -2110,7 +2110,7 @@ class HealthRawStressLocalStore { @@ -2110,7 +2110,7 @@ class HealthRawStressLocalStore {
2110 final db = await factory.openDatabase( 2110 final db = await factory.openDatabase(
2111 path, 2111 path,
2112 options: OpenDatabaseOptions( 2112 options: OpenDatabaseOptions(
2113 - version: 8, 2113 + version: 9,
2114 onCreate: (db, version) async { 2114 onCreate: (db, version) async {
2115 await _createTables(db); 2115 await _createTables(db);
2116 }, 2116 },
@@ -2137,6 +2137,9 @@ class HealthRawStressLocalStore { @@ -2137,6 +2137,9 @@ class HealthRawStressLocalStore {
2137 if (oldVersion < 8) { 2137 if (oldVersion < 8) {
2138 await _addUpdateTimeColumns(db); 2138 await _addUpdateTimeColumns(db);
2139 } 2139 }
  2140 + if (oldVersion < 9) {
  2141 + await _addDateKeyColumns(db);
  2142 + }
2140 }, 2143 },
2141 ), 2144 ),
2142 ); 2145 );
@@ -2148,6 +2151,7 @@ class HealthRawStressLocalStore { @@ -2148,6 +2151,7 @@ class HealthRawStressLocalStore {
2148 await db.execute(''' 2151 await db.execute('''
2149 CREATE TABLE IF NOT EXISTS $hrvResultsTable ( 2152 CREATE TABLE IF NOT EXISTS $hrvResultsTable (
2150 raw_end_time INTEGER PRIMARY KEY, 2153 raw_end_time INTEGER PRIMARY KEY,
  2154 + date_key INTEGER NOT NULL DEFAULT 0,
2151 user_id INTEGER NOT NULL, 2155 user_id INTEGER NOT NULL,
2152 raw_hrv REAL NOT NULL DEFAULT 0, 2156 raw_hrv REAL NOT NULL DEFAULT 0,
2153 result REAL NOT NULL, 2157 result REAL NOT NULL,
@@ -2169,6 +2173,7 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable ( @@ -2169,6 +2173,7 @@ CREATE TABLE IF NOT EXISTS $hrvResultsTable (
2169 await db.execute(''' 2173 await db.execute('''
2170 CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( 2174 CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2171 raw_end_time INTEGER PRIMARY KEY, 2175 raw_end_time INTEGER PRIMARY KEY,
  2176 + date_key INTEGER NOT NULL DEFAULT 0,
2172 user_id INTEGER NOT NULL, 2177 user_id INTEGER NOT NULL,
2173 raw_hr REAL NOT NULL DEFAULT 0, 2178 raw_hr REAL NOT NULL DEFAULT 0,
2174 result REAL NOT NULL, 2179 result REAL NOT NULL,
@@ -2190,6 +2195,7 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( @@ -2190,6 +2195,7 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable (
2190 await db.execute(''' 2195 await db.execute('''
2191 CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( 2196 CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
2192 date INTEGER PRIMARY KEY, 2197 date INTEGER PRIMARY KEY,
  2198 + date_key INTEGER NOT NULL DEFAULT 0,
2193 user_id INTEGER NOT NULL, 2199 user_id INTEGER NOT NULL,
2194 stress_value REAL NOT NULL, 2200 stress_value REAL NOT NULL,
2195 stress_score INTEGER NOT NULL, 2201 stress_score INTEGER NOT NULL,
@@ -2205,6 +2211,7 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable ( @@ -2205,6 +2211,7 @@ CREATE TABLE IF NOT EXISTS $dailyStressResultsTable (
2205 await db.execute(''' 2211 await db.execute('''
2206 CREATE TABLE IF NOT EXISTS $sleepResultsTable ( 2212 CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2207 date INTEGER PRIMARY KEY, 2213 date INTEGER PRIMARY KEY,
  2214 + date_key INTEGER NOT NULL DEFAULT 0,
2208 user_id INTEGER NOT NULL, 2215 user_id INTEGER NOT NULL,
2209 start_date INTEGER NOT NULL, 2216 start_date INTEGER NOT NULL,
2210 sleep_score INTEGER NOT NULL, 2217 sleep_score INTEGER NOT NULL,
@@ -2235,6 +2242,44 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2235,6 +2242,44 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2235 } 2242 }
2236 } 2243 }
2237 2244
  2245 + Future<void> _backfillDateKey(DatabaseExecutor db, String table) async {
  2246 + final timeColumn = switch (table) {
  2247 + hrvResultsTable || realtimeStressResultsTable => 'raw_end_time',
  2248 + dailyStressResultsTable || sleepResultsTable => 'date',
  2249 + _ => null,
  2250 + };
  2251 + if (timeColumn == null) return;
  2252 + if (table == dailyStressResultsTable) {
  2253 + await db.execute(
  2254 + 'UPDATE $table SET date_key = date WHERE date_key = 0 AND date > 0',
  2255 + );
  2256 + return;
  2257 + }
  2258 + await db.execute(
  2259 + "UPDATE $table SET date_key = CAST(strftime('%Y%m%d', "
  2260 + "$timeColumn, 'unixepoch', 'localtime') AS INTEGER) "
  2261 + 'WHERE date_key = 0 AND $timeColumn > 0',
  2262 + );
  2263 + }
  2264 +
  2265 + Future<void> _addDateKeyColumns(DatabaseExecutor db) async {
  2266 + for (final table in const [
  2267 + hrvResultsTable,
  2268 + realtimeStressResultsTable,
  2269 + dailyStressResultsTable,
  2270 + sleepResultsTable,
  2271 + ]) {
  2272 + try {
  2273 + await db.execute(
  2274 + 'ALTER TABLE $table ADD COLUMN date_key INTEGER NOT NULL DEFAULT 0',
  2275 + );
  2276 + } on DatabaseException catch (error) {
  2277 + if (!error.isDuplicateColumnError()) rethrow;
  2278 + }
  2279 + await _backfillDateKey(db, table);
  2280 + }
  2281 + }
  2282 +
2238 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async { 2283 Future<void> _addFlagColumns(DatabaseExecutor db, String table) async {
2239 for (final column in const [ 2284 for (final column in const [
2240 'is_workout', 2285 'is_workout',
@@ -2381,9 +2426,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2381,9 +2426,15 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2381 return now.year * 10000 + now.month * 100 + now.day; 2426 return now.year * 10000 + now.month * 100 + now.day;
2382 } 2427 }
2383 2428
  2429 + int _dateKeyFromUnixSeconds(int seconds) {
  2430 + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  2431 + return date.year * 10000 + date.month * 100 + date.day;
  2432 + }
  2433 +
2384 Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) { 2434 Map<String, Object?> _hrvRow(HealthRawHrvStressPoint point) {
2385 return <String, Object?>{ 2435 return <String, Object?>{
2386 'raw_end_time': point.rawEndTime, 2436 'raw_end_time': point.rawEndTime,
  2437 + 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
2387 'user_id': point.userId, 2438 'user_id': point.userId,
2388 'raw_hrv': point.rawHrv, 2439 'raw_hrv': point.rawHrv,
2389 'result': _integerDouble(point.result), 2440 'result': _integerDouble(point.result),
@@ -2403,6 +2454,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2403,6 +2454,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2403 Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) { 2454 Map<String, Object?> _realtimeRow(HealthRawRealtimeStressPoint point) {
2404 return <String, Object?>{ 2455 return <String, Object?>{
2405 'raw_end_time': point.rawEndTime, 2456 'raw_end_time': point.rawEndTime,
  2457 + 'date_key': _dateKeyFromUnixSeconds(point.rawEndTime),
2406 'user_id': point.userId, 2458 'user_id': point.userId,
2407 'raw_hr': point.rawHr, 2459 'raw_hr': point.rawHr,
2408 'result': _integerDouble(point.result), 2460 'result': _integerDouble(point.result),
@@ -2417,6 +2469,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2417,6 +2469,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2417 Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) { 2469 Map<String, Object?> _dailyStressRow(HealthRawDailyStressPoint point) {
2418 return <String, Object?>{ 2470 return <String, Object?>{
2419 'date': point.date, 2471 'date': point.date,
  2472 + 'date_key': point.date,
2420 'user_id': point.userId, 2473 'user_id': point.userId,
2421 'stress_value': _integerDouble(point.stressValue), 2474 'stress_value': _integerDouble(point.stressValue),
2422 'stress_score': point.stressScore, 2475 'stress_score': point.stressScore,
@@ -2430,6 +2483,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2430,6 +2483,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2430 Map<String, Object?> _sleepRow(HealthRawSleepResult result) { 2483 Map<String, Object?> _sleepRow(HealthRawSleepResult result) {
2431 return <String, Object?>{ 2484 return <String, Object?>{
2432 'date': result.date, 2485 'date': result.date,
  2486 + 'date_key': _dateKeyFromUnixSeconds(result.date),
2433 'user_id': result.userId, 2487 'user_id': result.userId,
2434 'start_date': result.startDate, 2488 'start_date': result.startDate,
2435 'sleep_score': result.sleepScore, 2489 'sleep_score': result.sleepScore,
@@ -2487,6 +2541,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2487,6 +2541,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2487 table, 2541 table,
2488 <String, Object?>{ 2542 <String, Object?>{
2489 'user_id': row['user_id'], 2543 'user_id': row['user_id'],
  2544 + 'date_key': row['date_key'],
2490 if (row.containsKey('raw_hrv')) 'raw_hrv': row['raw_hrv'], 2545 if (row.containsKey('raw_hrv')) 'raw_hrv': row['raw_hrv'],
2491 if (row.containsKey('raw_hr')) 'raw_hr': row['raw_hr'], 2546 if (row.containsKey('raw_hr')) 'raw_hr': row['raw_hr'],
2492 'result': row['result'], 2547 'result': row['result'],
@@ -2586,6 +2641,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2586,6 +2641,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2586 dailyStressResultsTable, 2641 dailyStressResultsTable,
2587 <String, Object?>{ 2642 <String, Object?>{
2588 'user_id': row['user_id'], 2643 'user_id': row['user_id'],
  2644 + 'date_key': row['date_key'],
2589 'stress_value': row['stress_value'], 2645 'stress_value': row['stress_value'],
2590 'stress_score': row['stress_score'], 2646 'stress_score': row['stress_score'],
2591 'state': row['state'], 2647 'state': row['state'],
@@ -2644,6 +2700,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2644,6 +2700,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2644 sleepResultsTable, 2700 sleepResultsTable,
2645 <String, Object?>{ 2701 <String, Object?>{
2646 'user_id': row['user_id'], 2702 'user_id': row['user_id'],
  2703 + 'date_key': row['date_key'],
2647 'start_date': row['start_date'], 2704 'start_date': row['start_date'],
2648 'sleep_score': row['sleep_score'], 2705 'sleep_score': row['sleep_score'],
2649 'sleep_state': row['sleep_state'], 2706 'sleep_state': row['sleep_state'],
@@ -2693,6 +2750,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable ( @@ -2693,6 +2750,7 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
2693 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) { 2750 List<String> _rawResultStoredValueKeys(Map<String, Object?> row) {
2694 return <String>[ 2751 return <String>[
2695 'user_id', 2752 'user_id',
  2753 + 'date_key',
2696 if (row.containsKey('raw_hrv')) 'raw_hrv', 2754 if (row.containsKey('raw_hrv')) 'raw_hrv',
2697 if (row.containsKey('raw_hr')) 'raw_hr', 2755 if (row.containsKey('raw_hr')) 'raw_hr',
2698 'result', 2756 'result',
@@ -281,8 +281,13 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore { @@ -281,8 +281,13 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
281 final db = await factory.openDatabase( 281 final db = await factory.openDatabase(
282 path, 282 path,
283 options: OpenDatabaseOptions( 283 options: OpenDatabaseOptions(
284 - version: 1, 284 + version: 2,
285 onCreate: (db, version) => _createTables(db), 285 onCreate: (db, version) => _createTables(db),
  286 + onUpgrade: (db, oldVersion, newVersion) async {
  287 + if (oldVersion < 2) {
  288 + await _addDateKeyColumns(db);
  289 + }
  290 + },
286 ), 291 ),
287 ); 292 );
288 _opened[userId] = db; 293 _opened[userId] = db;
@@ -306,6 +311,7 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable ( @@ -306,6 +311,7 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
306 data_type INTEGER, 311 data_type INTEGER,
307 from_time INTEGER NOT NULL, 312 from_time INTEGER NOT NULL,
308 to_time INTEGER NOT NULL, 313 to_time INTEGER NOT NULL,
  314 + date_key INTEGER NOT NULL DEFAULT 0,
309 create_time INTEGER NOT NULL, 315 create_time INTEGER NOT NULL,
310 UNIQUE (from_time) 316 UNIQUE (from_time)
311 ) 317 )
@@ -342,6 +348,7 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -342,6 +348,7 @@ CREATE TABLE IF NOT EXISTS $table (
342 time INTEGER NOT NULL, 348 time INTEGER NOT NULL,
343 value REAL, 349 value REAL,
344 is_asleep INTEGER, 350 is_asleep INTEGER,
  351 + date_key INTEGER NOT NULL DEFAULT 0,
345 create_time INTEGER NOT NULL, 352 create_time INTEGER NOT NULL,
346 UNIQUE (time) 353 UNIQUE (time)
347 ) 354 )
@@ -359,6 +366,7 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -359,6 +366,7 @@ CREATE TABLE IF NOT EXISTS $table (
359 activity_type INTEGER, 366 activity_type INTEGER,
360 from_time INTEGER NOT NULL, 367 from_time INTEGER NOT NULL,
361 to_time INTEGER NOT NULL, 368 to_time INTEGER NOT NULL,
  369 + date_key INTEGER NOT NULL DEFAULT 0,
362 create_time INTEGER NOT NULL, 370 create_time INTEGER NOT NULL,
363 UNIQUE (from_time) 371 UNIQUE (from_time)
364 ) 372 )
@@ -372,10 +380,12 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -372,10 +380,12 @@ CREATE TABLE IF NOT EXISTS $table (
372 OhosHealthRawDataItem item, 380 OhosHealthRawDataItem item,
373 int createTime, 381 int createTime,
374 ) { 382 ) {
  383 + final time = _numPayload(item.payload, 'time') ?? item.dataTime;
375 return { 384 return {
376 - 'time': _numPayload(item.payload, 'time') ?? item.dataTime, 385 + 'time': time,
377 'value': _numPayload(item.payload, 'value'), 386 'value': _numPayload(item.payload, 'value'),
378 'is_asleep': _numPayload(item.payload, 'is_asleep'), 387 'is_asleep': _numPayload(item.payload, 'is_asleep'),
  388 + 'date_key': _dateKeyFromUnixSeconds(time),
379 'create_time': createTime, 389 'create_time': createTime,
380 }; 390 };
381 } 391 }
@@ -384,14 +394,16 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -384,14 +394,16 @@ CREATE TABLE IF NOT EXISTS $table (
384 OhosHealthRawDataItem item, 394 OhosHealthRawDataItem item,
385 int createTime, 395 int createTime,
386 ) { 396 ) {
  397 + final toTime = _numPayload(item.payload, 'to_time') ??
  398 + _numPayload(item.payload, 'end_time') ??
  399 + item.dataTime;
387 return { 400 return {
388 'activity_type': _numPayload(item.payload, 'activity_type'), 401 'activity_type': _numPayload(item.payload, 'activity_type'),
389 'from_time': _numPayload(item.payload, 'from_time') ?? 402 'from_time': _numPayload(item.payload, 'from_time') ??
390 _numPayload(item.payload, 'start_time') ?? 403 _numPayload(item.payload, 'start_time') ??
391 0, 404 0,
392 - 'to_time': _numPayload(item.payload, 'to_time') ??  
393 - _numPayload(item.payload, 'end_time') ??  
394 - item.dataTime, 405 + 'to_time': toTime,
  406 + 'date_key': _dateKeyFromUnixSeconds(toTime),
395 'create_time': createTime, 407 'create_time': createTime,
396 }; 408 };
397 } 409 }
@@ -400,10 +412,12 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -400,10 +412,12 @@ CREATE TABLE IF NOT EXISTS $table (
400 OhosHealthRawDataItem item, 412 OhosHealthRawDataItem item,
401 int createTime, 413 int createTime,
402 ) { 414 ) {
  415 + final toTime = _numPayload(item.payload, 'to_time') ?? item.dataTime;
403 return { 416 return {
404 'data_type': _numPayload(item.payload, 'data_type') ?? item.dataType, 417 'data_type': _numPayload(item.payload, 'data_type') ?? item.dataType,
405 'from_time': _numPayload(item.payload, 'from_time') ?? 0, 418 'from_time': _numPayload(item.payload, 'from_time') ?? 0,
406 - 'to_time': _numPayload(item.payload, 'to_time') ?? item.dataTime, 419 + 'to_time': toTime,
  420 + 'date_key': _dateKeyFromUnixSeconds(toTime),
407 'create_time': createTime, 421 'create_time': createTime,
408 }; 422 };
409 } 423 }
@@ -431,6 +445,7 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -431,6 +445,7 @@ CREATE TABLE IF NOT EXISTS $table (
431 'time': dataTime, 445 'time': dataTime,
432 'value': row['value'], 446 'value': row['value'],
433 'is_asleep': row['is_asleep'], 447 'is_asleep': row['is_asleep'],
  448 + 'date_key': row['date_key'],
434 }, 449 },
435 ); 450 );
436 } 451 }
@@ -446,6 +461,7 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -446,6 +461,7 @@ CREATE TABLE IF NOT EXISTS $table (
446 'data_type': dataType, 461 'data_type': dataType,
447 'from_time': row['from_time'], 462 'from_time': row['from_time'],
448 'to_time': toTime, 463 'to_time': toTime,
  464 + 'date_key': row['date_key'],
449 }, 465 },
450 ); 466 );
451 } 467 }
@@ -462,6 +478,7 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -462,6 +478,7 @@ CREATE TABLE IF NOT EXISTS $table (
462 'activity_type': row['activity_type'], 478 'activity_type': row['activity_type'],
463 'from_time': row['from_time'], 479 'from_time': row['from_time'],
464 'to_time': toTime, 480 'to_time': toTime,
  481 + 'date_key': row['date_key'],
465 }, 482 },
466 ); 483 );
467 } 484 }
@@ -526,6 +543,61 @@ CREATE TABLE IF NOT EXISTS $table ( @@ -526,6 +543,61 @@ CREATE TABLE IF NOT EXISTS $table (
526 1000; 543 1000;
527 } 544 }
528 545
  546 + int _dateKeyFromUnixSeconds(int seconds) {
  547 + final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  548 + return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
  549 + }
  550 +
  551 + Future<void> _addDateKeyColumns(DatabaseExecutor db) async {
  552 + final rows = await db.query(
  553 + 'sqlite_master',
  554 + columns: ['name'],
  555 + where: "type = 'table' AND (name = ? OR name LIKE ?)",
  556 + whereArgs: [sleepDataTable, '${rawDataTable}_%'],
  557 + );
  558 + for (final row in rows) {
  559 + final table = row['name'] as String?;
  560 + if (table == null) continue;
  561 + await _addColumnIfMissing(
  562 + db,
  563 + table,
  564 + 'date_key INTEGER NOT NULL DEFAULT 0',
  565 + );
  566 + await _backfillDateKey(db, table);
  567 + }
  568 + }
  569 +
  570 + Future<void> _backfillDateKey(DatabaseExecutor db, String table) async {
  571 + final columns = await db.rawQuery('PRAGMA table_info($table)');
  572 + final columnNames =
  573 + columns.map((row) => row['name']).whereType<String>().toSet();
  574 + final timeColumn = columnNames.contains('to_time') ? 'to_time' : 'time';
  575 + if (!columnNames.contains(timeColumn)) return;
  576 + await db.execute(
  577 + "UPDATE $table SET date_key = CAST(strftime('%Y%m%d', "
  578 + "$timeColumn, 'unixepoch', 'localtime') AS INTEGER) "
  579 + 'WHERE date_key = 0 AND $timeColumn > 0',
  580 + );
  581 + }
  582 +
  583 + Future<void> _addColumnIfMissing(
  584 + DatabaseExecutor db,
  585 + String table,
  586 + String columnSql,
  587 + ) async {
  588 + try {
  589 + await db.execute('ALTER TABLE $table ADD COLUMN $columnSql');
  590 + } on DatabaseException catch (error) {
  591 + if (!_isDuplicateColumnError(error)) rethrow;
  592 + }
  593 + }
  594 +
  595 + bool _isDuplicateColumnError(DatabaseException error) {
  596 + final message = error.toString().toLowerCase();
  597 + return message.contains('duplicate column') ||
  598 + message.contains('duplicate column name');
  599 + }
  600 +
529 int? _numPayload(Map<String, Object?> payload, String key) { 601 int? _numPayload(Map<String, Object?> payload, String key) {
530 final value = payload[key]; 602 final value = payload[key];
531 return value is num ? value.toInt() : null; 603 return value is num ? value.toInt() : null;
@@ -293,7 +293,8 @@ class OhosHealthRawDataSyncService { @@ -293,7 +293,8 @@ class OhosHealthRawDataSyncService {
293 'chunkDays=$fetchChunkDays ' 293 'chunkDays=$fetchChunkDays '
294 'startTime=${range.startTime} endTime=${range.endTime} ' 294 'startTime=${range.startTime} endTime=${range.endTime} '
295 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} ' 295 'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '
296 - 'endDate=${_dateKeyFromUnixSeconds(range.endTime)}', 296 + 'endDate=${_dateKeyFromUnixSeconds(range.endTime)} '
  297 + 'apiEndDate=${_exclusiveEndDateKeyFromUnixSeconds(range.endTime)}',
297 ); 298 );
298 299
299 final page = await _remoteDataSource.fetchRawDataPage( 300 final page = await _remoteDataSource.fetchRawDataPage(
@@ -501,10 +502,9 @@ class OhosHarmonyHealthRawDataRemoteDataSource @@ -501,10 +502,9 @@ class OhosHarmonyHealthRawDataRemoteDataSource
501 OhosHarmonyHealthRawDataRemoteDataSource( 502 OhosHarmonyHealthRawDataRemoteDataSource(
502 this._client, { 503 this._client, {
503 DateTime Function()? nowProvider, 504 DateTime Function()? nowProvider,
504 - }) : _nowProvider = nowProvider ?? DateTime.now; 505 + });
505 506
506 final OhosHarmonyRawDataClient _client; 507 final OhosHarmonyRawDataClient _client;
507 - final DateTime Function() _nowProvider;  
508 508
509 @override 509 @override
510 Future<OhosHealthRawDataPage> fetchRawDataPage({ 510 Future<OhosHealthRawDataPage> fetchRawDataPage({
@@ -513,7 +513,7 @@ class OhosHarmonyHealthRawDataRemoteDataSource @@ -513,7 +513,7 @@ class OhosHarmonyHealthRawDataRemoteDataSource
513 required int endTime, 513 required int endTime,
514 }) async { 514 }) async {
515 final startDate = _dateKeyFromUnixSeconds(startTime); 515 final startDate = _dateKeyFromUnixSeconds(startTime);
516 - final endDate = _endDateKeyFromUnixSeconds(endTime); 516 + final endDate = _exclusiveEndDateKeyFromUnixSeconds(endTime);
517 if (dataType == OhosHealthRawDataType.sleepAnalysis) { 517 if (dataType == OhosHealthRawDataType.sleepAnalysis) {
518 final result = await _client.getSleepData(startDate, endDate); 518 final result = await _client.getSleepData(startDate, endDate);
519 return switch (result) { 519 return switch (result) {
@@ -630,17 +630,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource @@ -630,17 +630,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource
630 ) 630 )
631 .toList(growable: false); 631 .toList(growable: false);
632 } 632 }
633 -  
634 - int _endDateKeyFromUnixSeconds(int seconds) {  
635 - final end = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);  
636 - final now = _nowProvider();  
637 - final today = DateTime(now.year, now.month, now.day);  
638 - final endDate = DateTime(end.year, end.month, end.day);  
639 - if (endDate == today) {  
640 - return _dateKeyFromDateTime(today.add(const Duration(days: 1)));  
641 - }  
642 - return _dateKeyFromDateTime(endDate);  
643 - }  
644 } 633 }
645 634
646 class TodoOhosHealthRawDataRemoteDataSource 635 class TodoOhosHealthRawDataRemoteDataSource
@@ -830,6 +819,12 @@ int _dateKeyFromUnixSeconds(int seconds) { @@ -830,6 +819,12 @@ int _dateKeyFromUnixSeconds(int seconds) {
830 return _dateKeyFromDateTime(dateTime); 819 return _dateKeyFromDateTime(dateTime);
831 } 820 }
832 821
  822 +int _exclusiveEndDateKeyFromUnixSeconds(int seconds) {
  823 + final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  824 + final endDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
  825 + return _dateKeyFromDateTime(endDate.add(const Duration(days: 1)));
  826 +}
  827 +
833 int _dateKeyFromDateTime(DateTime dateTime) { 828 int _dateKeyFromDateTime(DateTime dateTime) {
834 return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day; 829 return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
835 } 830 }
@@ -106,6 +106,9 @@ void main() { @@ -106,6 +106,9 @@ void main() {
106 expect(result.segmentCount, remote.calls.length); 106 expect(result.segmentCount, remote.calls.length);
107 expect(remote.calls.first.startTime, expectedStart); 107 expect(remote.calls.first.startTime, expectedStart);
108 expect(remote.calls.last.endTime, expectedEnd); 108 expect(remote.calls.last.endTime, expectedEnd);
  109 + for (var i = 1; i < remote.calls.length; i += 1) {
  110 + expect(remote.calls[i].startTime, remote.calls[i - 1].endTime + 1);
  111 + }
109 expect(local.storedBatches, isEmpty); 112 expect(local.storedBatches, isEmpty);
110 }); 113 });
111 114
@@ -480,13 +483,28 @@ void main() { @@ -480,13 +483,28 @@ void main() {
480 483
481 expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv); 484 expect(client.healthCalls.single.dataType, HuaweiHealthDataType.hrv);
482 expect(client.healthCalls.single.startDate, 20260801); 485 expect(client.healthCalls.single.startDate, 20260801);
483 - expect(client.healthCalls.single.endDate, 20260831); 486 + expect(client.healthCalls.single.endDate, 20260901);
484 expect(page.items, hasLength(1)); 487 expect(page.items, hasLength(1));
485 expect(page.items.single.dataType, 1); 488 expect(page.items.single.dataType, 1);
486 expect(page.items.single.dataTime, 1787900000); 489 expect(page.items.single.dataTime, 1787900000);
487 expect(page.items.single.payload['value'], 53); 490 expect(page.items.single.payload['value'], 53);
488 }); 491 });
489 492
  493 + test('Harmony remote data source uses exclusive endDate for historical day',
  494 + () async {
  495 + final client = _FakeOhosHarmonyRawDataClient();
  496 + final remote = OhosHarmonyHealthRawDataRemoteDataSource(client);
  497 +
  498 + await remote.fetchRawDataPage(
  499 + dataType: HuaweiHealthDataType.hrv.dataType,
  500 + startTime: _unixSeconds(DateTime(2026, 8, 30)),
  501 + endTime: _unixSeconds(DateTime(2026, 8, 30, 23, 59, 59)),
  502 + );
  503 +
  504 + expect(client.healthCalls.single.startDate, 20260830);
  505 + expect(client.healthCalls.single.endDate, 20260831);
  506 + });
  507 +
490 test('Harmony remote data source uses tomorrow as endDate for today data', 508 test('Harmony remote data source uses tomorrow as endDate for today data',
491 () async { 509 () async {
492 final client = _FakeOhosHarmonyRawDataClient(); 510 final client = _FakeOhosHarmonyRawDataClient();
@@ -530,7 +548,7 @@ void main() { @@ -530,7 +548,7 @@ void main() {
530 ); 548 );
531 549
532 expect(client.sleepCalls.single.startDate, 20260801); 550 expect(client.sleepCalls.single.startDate, 20260801);
533 - expect(client.sleepCalls.single.endDate, 20260802); 551 + expect(client.sleepCalls.single.endDate, 20260803);
534 expect(page.items, hasLength(1)); 552 expect(page.items, hasLength(1));
535 expect(page.items.single.dataType, 4); 553 expect(page.items.single.dataType, 4);
536 expect(page.items.single.dataTime, 1787870000); 554 expect(page.items.single.dataTime, 1787870000);