新增系统日历镜像功能,镜像与备份目标统一改用 bookId;修复重复日程覆盖行、备份目标随勾选漂移等一批问题

- 新增 SystemCalendarMirror / MirrorSnapshot / MirrorInbound:把选定的 CalDAV 日历本镜像写入系统日历(供小艺、桌面日历卡片、手表可见),并做入站对账,区分"自己写入的 / 用户改的 / 用户删的 / 用户新建的",镜像标识一律用稳定的 bookId(accId-href哈希),不再用会随勾选顺序漂移的 calKey
- 后台同步接入镜像:同步前先做入站对账、同步后自动出站镜像,无需用户手动触发
- 系统日历备份目标改用 bookId:当用户取消同步某个日历本导致目标失效时,自动清空目标并把模式回退为"仅显示";开启备份必须先选定目标本,不再自动挑第一个本
- EventDb:覆盖行保留 RECURRENCE-ID,排除主事件对应发生改用 overrideKey(calKey, uid, recurrenceId, startTime),修复重复日程出现"幽灵条目"的问题
- 新增 WRITE_CALENDAR 权限声明及其用途说明;添加日历不再出现"本机",日历本对齐显示;版本升至 0.0.3(300)
This commit is contained in:
laoyang
2026-09-19 23:07:03 +08:00
parent 165740fe9e
commit 736a0a6587
16 changed files with 2815 additions and 84 deletions
@@ -5,10 +5,10 @@
import { common } from '@kit.AbilityKit';
import { calendarManager } from '@kit.CalendarKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AccountStore, DavAccount } from './AccountStore';
import { EventDb, LocalEvent } from './EventDb';
import { AppSettings } from './AppSettings';
import { LogUtil } from './LogUtil';
import { BookRef, SystemCalendarMirror } from './SystemCalendarMirror';
export class SystemCalendarImport {
/**
@@ -16,6 +16,15 @@ export class SystemCalendarImport {
* 幂等:uid = syscal-<系统日历id>-<事件id>,已存在则跳过,因此每次同步都可安全执行。
* 只读取 calendarType = LOCAL 的系统日历(手机本地日历/应用创建的日程),
* 系统 CalDAV 同步产生的账户日历一律跳过,避免死循环回灌。
*
* ⚠️ 另有两层防重复(09-18 补):
* 1. **跳过本应用镜像出去的账户**(`synccalendar.mirror.*`)——它们的 type 也是 LOCAL,
* 但内容是我们从 CalDAV 写进去的,再导进备份本会让同一条日程在服务器出现两份。
* 2. 导进备份本的 uid 统一带 `syscal-` 前缀,出站镜像会跳过该前缀 → 备份本不会被镜像回系统日历。
*
* ⚠️ 目标本用**稳定标识 bookId** 解析(`SystemCalendarMirror.backupTargetRef`):
* 用户「取消勾选」目标本后序号会整体前移,老版本按 `accId_序号` 定位会**导进另一个本**。
* 现在解析不到即视为失效,并把「备份到 CalDAV」模式**自动关回仅显示**。
*/
static async importIfNeeded(context: common.Context): Promise<number> {
try {
@@ -23,30 +32,42 @@ export class SystemCalendarImport {
if (mode !== 'backup') {
return 0;
}
const calKey: string = await AppSettings.getBackupCalKey(context);
if (calKey === '') {
LogUtil.write('系统日历备份:未选择目标日历本,跳过导入');
// ⭐ 目标本一律走 `backupTargetRef()`:内部用**稳定标识 bookId** 解析,
// 并兼做「老 calKey 值 → bookId」的一次性迁移。
// 解析不到时它会**自动把 sys_cal_mode 关回 display**(用户明确要求的行为):
// 目标本被取消同步/账号被删后,绝不能"随便挑一个本"把系统日历导进去。
const ref: BookRef | undefined = await SystemCalendarMirror.backupTargetRef(context);
if (ref === undefined) {
// 未选择目标本(或刚被判失效并关闭)→ 跳过。原因已由 backupTargetRef 写进日志。
LogUtil.write('系统日历备份:当前无有效目标日历本,跳过导入');
return 0;
}
// 定位目标 CalDAV 日历本
const accounts: DavAccount[] = await AccountStore.loadAll(context);
let targetHref: string = '';
for (const acc of accounts) {
for (let i = 0; i < acc.calendarHrefs.length; i++) {
if (`${acc.id}_${i}` === calKey) {
targetHref = acc.calendarHrefs[i];
}
}
}
const calKey: string = ref.calKey; // 仅用于查本地库 / 落库
const targetHref: string = ref.href;
if (targetHref === '') {
LogUtil.write(`系统日历备份:目标日历本 ${calKey} 不存在(账号可能已删除),跳过导入`);
LogUtil.write(`系统日历备份:目标日历本 ${ref.bookId} 缺少集合 URL,跳过导入`);
return 0;
}
const uiContext = context as common.UIAbilityContext;
const mgr: calendarManager.CalendarManager = calendarManager.getCalendarManager(uiContext);
const calendars: calendarManager.Calendar[] = await mgr.getAllCalendars();
// 诊断:把 getAllCalendars() 实际能读到的账户全列出来。
// 用于确认"是否只能读到本应用创建的 + 系统默认账户",还是能读到其他应用自建的日历账户。
const brief: string[] = [];
for (const cal of calendars) {
let acc: calendarManager.CalendarAccount;
try {
acc = cal.getAccount();
} catch (err) {
continue;
}
const tag: string = SystemCalendarMirror.isMirrorAccountName(acc.name) ? '(我们的镜像)' : '';
brief.push(`${acc.name}[type=${acc.type}]${tag}`);
}
LogUtil.write(`系统日历账户清单:共 ${calendars.length} 个 → ${brief.join(' | ')}`);
let imported: number = 0;
let skippedCal: number = 0;
let skippedMirror: number = 0; // 本应用镜像出去的账户(不能导进备份本,否则重复)
for (const cal of calendars) {
try {
// 只处理"本地"类型系统日历;CalDAV/订阅等账户日历的数据源本来就在服务器上,跳过防回灌
@@ -55,6 +76,13 @@ export class SystemCalendarImport {
skippedCal++;
continue;
}
// ⚠️ 关键:跳过**本应用自己镜像出去的账户**(type 同样 LOCAL,但内容是我们从 CalDAV 写进去的)。
// 若一并导入备份本,同一条日程会在服务器里出现两份(原始本一份、备份本一份)→ 数据非常乱。
// 这些账户的改动由 MirrorInbound 负责回写到它**原本所属**的日历本,不走备份通道。
if (SystemCalendarMirror.isMirrorAccountName(account.name)) {
skippedMirror++;
continue;
}
const events: calendarManager.Event[] = await cal.getEvents();
for (const ev of events) {
if (ev.id === undefined) {
@@ -86,8 +114,9 @@ export class SystemCalendarImport {
// 单个日历读取失败不影响其余
}
}
if (imported > 0 || skippedCal > 0) {
LogUtil.write(`系统日历备份:新增 ${imported} 条 → ${calKey}(跳过账户日历 ${skippedCal} 个)`);
if (imported > 0 || skippedCal > 0 || skippedMirror > 0) {
LogUtil.write(
`系统日历备份:新增 ${imported} 条 → ${calKey}(跳过非本地账户 ${skippedCal} 个、镜像账户 ${skippedMirror} 个)`);
}
return imported;
} catch (err) {