v83: 后台定期同步(四层调度,策略照 SyncCalendar)

用户原话:「另外,要有定期同步的功能,就是家书app在后台,也能自动同步
数据,这样打开app,基本上数据就是最新的。同步的策略,去synccalendar找,
里面有办法。」

四层调度(缺一层就会在某种场景下"永远不同步"):
1. 前台:onForeground 时若距上次同步超过 5 分钟,补一次
2. 退后台:requestSuspendDelay 短时任务(单次≤3分钟、单日10分钟配额)
3. 延迟任务:workScheduler(周期 2 小时 —— 系统按活跃分组压制,
   请求更短不报错但不生效)
4. 桌面卡片:updateDuration:1(=30 分钟,唯一不受压制的通道,
   每天上限 50 次);先推卡片再触发同步

关键约束(写错了都不报错,只是静默失效):
- WorkSchedulerExtensionAbility 单次回调上限 2 分钟,必须在 finally 里 stopWork
- 后台扩展跑在独立进程,EntryAbility.onCreate 的初始化一步都没跑过
  ⇒ prepare() 必须补齐 token store / module token / FbDb.open
- 回前台必须 cancelSuspendDelay 归还配额,否则两三天后用光
- workInfo 要带 isPersisted,否则重启后任务永久消失
- abilityName 必须与 module.json5 声明一致,对不上时 startWork 不报错但不回调
- ApplicationInfo 没有 bundleName 字段,包名在 .name

其他:
- 卡片驱动的同步有节流(CARD_MIN_GAP_MS 25 分钟 < 卡片刷新 30 分钟,
  反了就是每次刷新都被跳过、一次都不跑)
- 设置页新增「同步」分组:开关 + 上次同步时间 + 说明

同时修:首页日程红线左侧缺一块(nowLayer 从 Row{圆点;线} 改成单个
Column 满宽 —— Row 里圆点占行首,线只能从第 5vp 起画)

守卫 1106/0、v83 反向验证 10/10 全中、已装机。
This commit is contained in:
2026-10-24 04:25:15 +08:00
parent 1c6af8ac56
commit 0bab0cdcc6
8 changed files with 809 additions and 30 deletions
+19 -15
View File
@@ -320,26 +320,30 @@ struct FbCard4x4 {
.height('100%')
}
/** 第 3 层:当前时间红线(只有今天画) */
/** 第 3 层:当前时间红线(只有今天画)
*
* 🔴🔴 v82(用户 2026-10-23):「鸿蒙端的首页日程,红线也短了,左侧缺一块。」
* 真根因(与同时修的网页端同一个形状):原来的红线是
* `Row { 4vp 圆点; 线(layoutWeight 1) }` —— **圆点占了行首那 4vp**,
* 线只能从第 5vp 起画。用户看到的就是"左端缺一块"。
* 而且用户在同一条消息里明确说了:「那个红线左侧是不是还有一个红点?
* 如果有,去掉。」⇒ 圆点本来就不该存在。
* ⇒ 现在整条线自己铺满 `width('100%')`,行首不再有任何占位元素。
* ⚠️ 别改回 `Row { 点; 线 }` 那种写法 —— 只要行首有子元素,线就一定短一截。
* ⚠️ `borderRadius(1)` 保留:2vp 的线加 1vp 圆角正好是胶囊形,
* 与网页端 `.ag-now` 的观感一致(网页端是 2px 无圆角,视觉上等价)。
*/
@Builder
private nowLayer() {
Column() {
if (this.nowOffsetVp() >= 0) {
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
Row() {
Column()
.width(4)
.height(4)
.borderRadius(2)
.backgroundColor('#C0533F')
Column()
.height(2)
.layoutWeight(1)
.borderRadius(1)
.backgroundColor('#C0533F')
}
.width('100%')
.hitTestBehavior(HitTestMode.None)
Column()
.width('100%')
.height(2)
.borderRadius(1)
.backgroundColor('#C0533F')
.hitTestBehavior(HitTestMode.None)
}
}
.width('100%')
+19 -15
View File
@@ -317,26 +317,30 @@ struct FbCard6x4 {
.height('100%')
}
/** 第 3 层:当前时间红线(只有今天画) */
/** 第 3 层:当前时间红线(只有今天画)
*
* 🔴🔴 v82(用户 2026-10-23):「鸿蒙端的首页日程,红线也短了,左侧缺一块。」
* 真根因(与同时修的网页端同一个形状):原来的红线是
* `Row { 4vp 圆点; 线(layoutWeight 1) }` —— **圆点占了行首那 4vp**,
* 线只能从第 5vp 起画。用户看到的就是"左端缺一块"。
* 而且用户在同一条消息里明确说了:「那个红线左侧是不是还有一个红点?
* 如果有,去掉。」⇒ 圆点本来就不该存在。
* ⇒ 现在整条线自己铺满 `width('100%')`,行首不再有任何占位元素。
* ⚠️ 别改回 `Row { 点; 线 }` 那种写法 —— 只要行首有子元素,线就一定短一截。
* ⚠️ `borderRadius(1)` 保留:2vp 的线加 1vp 圆角正好是胶囊形,
* 与网页端 `.ag-now` 的观感一致(网页端是 2px 无圆角,视觉上等价)。
*/
@Builder
private nowLayer() {
Column() {
if (this.nowOffsetVp() >= 0) {
Blank().height(this.nowOffsetVp()).hitTestBehavior(HitTestMode.None)
Row() {
Column()
.width(4)
.height(4)
.borderRadius(2)
.backgroundColor('#C0533F')
Column()
.height(2)
.layoutWeight(1)
.borderRadius(1)
.backgroundColor('#C0533F')
}
.width('100%')
.hitTestBehavior(HitTestMode.None)
Column()
.width('100%')
.height(2)
.borderRadius(1)
.backgroundColor('#C0533F')
.hitTestBehavior(HitTestMode.None)
}
}
.width('100%')
@@ -29,6 +29,7 @@ import { BusinessError } from '@kit.BasicServicesKit';
import { fbApi } from '../../data/FbHttp';
import { FB_SESSION } from '../../data/FbSession';
import { FB_DEVICE_STORE, FB_APP_VERSION } from '../../data/FbDeviceStore';
import { FB_BG_SYNC } from '../../common/FbBackgroundSync';
const DOMAIN: number = 0x0000;
const TAG: string = 'FbSettings';
@@ -54,6 +55,17 @@ export struct FbSettingsPage {
@Local tokenNote: string = '';
/** 「手动粘贴令牌」高级入口是否展开(v55:已不是主路径) */
@Local advancedOpen: boolean = false;
/**
* 🔴 v83「后台自动同步」开关的界面值。
*
* ⚠️ 不能直接读 `FB_BG_SYNC.isEnabled()`(它是 async / 读盘)——
* ArkUI 的构建函数里放不了异步取值,而且读盘结果回来前会先渲染一帧默认值。
* ⇒ 照本项目一贯的做法:`aboutToAppear` 里异步读一次落进 `@Local`,
* 用户拨动时先改内存再异步落盘(拨了立刻有反馈)。
*/
@Local bgSyncOn: boolean = true;
/** 上次同步时间的人话('' = 还没读过 / 从没同步过) */
@Local bgLastNote: string = '';
/** 设置页的主题色(与模块「个人设置」一致,取砖红) */
private accent: string = '#A84A28';
@@ -65,6 +77,80 @@ export struct FbSettingsPage {
*/
aboutToAppear(): void {
this.tokenNote = '';
this.loadBgSync();
}
/** 读后台同步开关 + 上次同步时间(异步一次,落进 @Local 供界面用) */
private loadBgSync(): void {
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
if (ctx === undefined) {
return;
}
FB_BG_SYNC.isEnabled(ctx).then((on: boolean): void => {
this.bgSyncOn = on;
}).catch((): void => {
});
FB_BG_SYNC.lastSyncAt(ctx).then((at: number): void => {
this.bgLastNote = this.agoText(at);
}).catch((): void => {
});
}
/**
* 把"上次同步时间戳"说成人话。
*
* ⚠️ 分档不要写太细("37 分钟前"这种精度对用户没意义,还容易显得一直不更新):
* 1 小时内按分钟、一天内按小时、再往上按天。
* ⚠️ `at <= 0` 是"从没同步过",不能说成"1970 年"。
*/
private agoText(at: number): string {
if (at <= 0) {
return '还没同步过';
}
const mins: number = Math.floor((Date.now() - at) / 60000);
if (mins < 1) {
return '刚刚';
}
if (mins < 60) {
return mins.toString() + ' 分钟前';
}
const hours: number = Math.floor(mins / 60);
if (hours < 24) {
return hours.toString() + ' 小时前';
}
return Math.floor(hours / 24).toString() + ' 天前';
}
/**
* 拨动「后台自动同步」开关。
*
* 🔴 v83(用户 2026-10-23「要有定期同步的功能,家书app在后台,也能自动同步数据,
* 这样打开app,基本上数据就是最新的」)。
* ⚠️ 先改内存再落盘:落盘要读 preferences(几十毫秒),
* 等它回来才动开关的话,用户会觉得"拨了没反应"。
* ⚠️ 关掉时要顺手取消延迟任务与短时任务(见 `FB_BG_SYNC.setEnabled`)——
* 只写一个标记而任务还在系统里排着,等于"关了还在后台偷偷跑"。
*/
private toggleBgSync(on: boolean): void {
this.bgSyncOn = on;
const ctx: common.Context | undefined = this.getUIContext().getHostContext();
if (ctx === undefined) {
return;
}
FB_BG_SYNC.setEnabled(ctx, on).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '写后台同步开关失败:%{public}s', JSON.stringify(err));
});
// 开启时立刻补一次(用户刚打开这个开关,多半就是想让数据现在对齐)
if (on) {
FB_BG_SYNC.runOnceResumed(ctx, 'foreground').then((): void => {
FB_BG_SYNC.lastSyncAt(ctx).then((at: number): void => {
this.bgLastNote = this.agoText(at);
}).catch((): void => {
});
}).catch((err: BusinessError): void => {
hilog.warn(DOMAIN, TAG, '开启后台同步后补同步失败:%{public}s', JSON.stringify(err));
});
}
}
@@ -415,6 +501,55 @@ export struct FbSettingsPage {
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
// ==================== 同步 ====================
/* 🔴 v83(用户 2026-10-23):「要有定期同步的功能,家书app在后台,
也能自动同步数据,这样打开app,基本上数据就是最新的。」
⇒ 这一组回答用户两个问题:**它开没开**(开关)、**上次什么时候**(时间)。
⚠️ 提示文案必须写清"多久一次",否则用户会以为"实时",
看到 30 分钟前的数据就认为坏了(分层调度的真实节奏见
`common/FbBackgroundSync.ets` 的文件头)。 */
Column({ space: 0 }) {
this.rowTitle('同步')
Row({ space: 12 }) {
Column({ space: 2 }) {
Text('后台自动同步')
.fontSize(FbMetric.fontSmall)
.fontColor(FbColors.ink)
Text('退到后台也自动对齐,约 30 分钟一次')
.fontSize(10)
.fontColor(FbColors.faint)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
Toggle({ type: ToggleType.Switch, isOn: this.bgSyncOn })
.selectedColor(FbColors.brand)
.onChange((on: boolean): void => {
this.toggleBgSync(on);
})
}
.width('100%')
.padding({ top: 9, bottom: 9 })
.alignItems(VerticalAlign.Center)
this.plainRow('上次同步', this.bgLastNote === '' ? '读取中…' : this.bgLastNote)
/* ⚠️ 说明为什么"不是即时":不给这句,用户切到别的应用改完再回来
没看到变化,就会认为同步坏了。 */
Text('回到家书时会立刻补一次;在别的应用里改的数据,最迟半小时能同步过来。')
.fontSize(10)
.fontColor(FbColors.faint)
.width('100%')
.maxLines(3)
.padding({ top: 2, bottom: 2 })
}
.padding(FbMetric.cardPad)
.backgroundColor(FbColors.surface)
.borderRadius(FbMetric.radius)
.margin({ left: FbMetric.pagePad, right: FbMetric.pagePad, top: 12 })
.alignItems(HorizontalAlign.Start)
// ==================== 抽屉里显示哪些模块 ====================
Column({ space: 0 }) {
Row({ space: 8 }) {