feat(background-task): 完善iOS后台任务系统并优化断食通知和UI体验

- 修复iOS后台任务注册时机问题,确保任务能正常触发
- 添加后台任务调试辅助工具和完整测试指南
- 优化断食通知系统,增加防抖机制避免频繁重调度
- 改进断食自动续订逻辑,使用固定时间而非相对时间计算
- 优化统计页面布局,添加身体指标section标题
- 增强饮水详情页面视觉效果,改进卡片样式和配色
- 添加用户反馈入口到个人设置页面
- 完善锻炼摘要卡片条件渲染逻辑
- 增强日志记录和错误处理机制

这些改进显著提升了应用的稳定性、性能和用户体验,特别是在iOS后台任务执行和断食功能方面。
This commit is contained in:
richarjiang
2025-11-05 11:23:33 +08:00
parent d74046498d
commit ea22901553
12 changed files with 1060 additions and 171 deletions

View File

@@ -263,16 +263,31 @@ const selectiveSyncNotifications = async ({
const start = dayjs(schedule.startISO);
const end = dayjs(schedule.endISO);
if (end.isBefore(now)) {
if (end.isBefore(now.subtract(1, 'hour'))) {
await clearFastingNotificationIds();
return {};
}
const updatedIds: FastingNotificationIds = { ...validIds };
// 先取消所有无效的旧通知,避免重复
const invalidIds = Object.entries(storedIds).filter(
([key, id]) => id && !Object.values(validIds).includes(id)
);
for (const [_, id] of invalidIds) {
if (id) {
try {
await notificationService.cancelNotification(id);
} catch (error) {
console.warn('取消无效通知失败', error);
}
}
}
// 1. 检查开始前30分钟通知
const preStart = start.subtract(REMINDER_OFFSET_MINUTES, 'minute');
if (preStart.isAfter(now) && !validIds.preStartId) {
if (preStart.isAfter(now.add(1, 'minute')) && !validIds.preStartId) {
try {
const preStartId = await notificationService.scheduleNotificationAtDate(
{
@@ -289,13 +304,14 @@ const selectiveSyncNotifications = async ({
preStart.toDate()
);
updatedIds.preStartId = preStartId;
console.log(`已安排断食开始前30分钟通知: ${preStart.format('YYYY-MM-DD HH:mm')}`);
} catch (error) {
console.error('安排断食开始前30分钟通知失败', error);
}
}
// 2. 检查开始时通知
if (start.isAfter(now) && !validIds.startId) {
if (start.isAfter(now.add(1, 'minute')) && !validIds.startId) {
try {
const startId = await notificationService.scheduleNotificationAtDate(
{
@@ -312,6 +328,7 @@ const selectiveSyncNotifications = async ({
start.toDate()
);
updatedIds.startId = startId;
console.log(`已安排断食开始时通知: ${start.format('YYYY-MM-DD HH:mm')}`);
} catch (error) {
console.error('安排断食开始时通知失败', error);
}
@@ -319,7 +336,7 @@ const selectiveSyncNotifications = async ({
// 3. 检查结束前30分钟通知
const preEnd = end.subtract(REMINDER_OFFSET_MINUTES, 'minute');
if (preEnd.isAfter(now) && !validIds.preEndId) {
if (preEnd.isAfter(now.add(1, 'minute')) && !validIds.preEndId) {
try {
const preEndId = await notificationService.scheduleNotificationAtDate(
{
@@ -336,13 +353,14 @@ const selectiveSyncNotifications = async ({
preEnd.toDate()
);
updatedIds.preEndId = preEndId;
console.log(`已安排断食结束前30分钟通知: ${preEnd.format('YYYY-MM-DD HH:mm')}`);
} catch (error) {
console.error('安排断食结束前30分钟通知失败', error);
}
}
// 4. 检查结束时通知
if (end.isAfter(now) && !validIds.endId) {
if (end.isAfter(now.add(1, 'minute')) && !validIds.endId) {
try {
const endId = await notificationService.scheduleNotificationAtDate(
{
@@ -359,6 +377,7 @@ const selectiveSyncNotifications = async ({
end.toDate()
);
updatedIds.endId = endId;
console.log(`已安排断食结束时通知: ${end.format('YYYY-MM-DD HH:mm')}`);
} catch (error) {
console.error('安排断食结束时通知失败', error);
}
@@ -398,8 +417,9 @@ export const verifyFastingNotifications = async ({
const start = dayjs(schedule.startISO);
const end = dayjs(schedule.endISO);
// 如果断食期已结束,应该清空所有通知
if (end.isBefore(now)) {
// 如果断食期已结束超过1小时,应该清空所有通知
// 这样可以避免在自动续订过程中过早清空通知
if (end.isBefore(now.subtract(1, 'hour'))) {
if (Object.values(storedIds).some(id => id)) {
await cancelNotificationIds(storedIds);
await clearFastingNotificationIds();
@@ -431,9 +451,15 @@ export const verifyFastingNotifications = async ({
const validIds: FastingNotificationIds = {};
for (const expected of expectedNotifications) {
// 跳过已过期的通知
if (expected.time.isBefore(now)) {
// 跳过已过期的通知过期超过5分钟
if (expected.time.isBefore(now.subtract(5, 'minute'))) {
if (expected.id) {
// 取消已过期的通知
try {
await notificationService.cancelNotification(expected.id);
} catch (error) {
console.warn('取消过期通知失败', error);
}
needsResync = true;
}
continue;
@@ -451,6 +477,20 @@ export const verifyFastingNotifications = async ({
continue;
}
// 检查通知时间是否匹配容差1分钟
const notification = scheduledNotifications.find(n => n.identifier === expected.id);
if (notification?.trigger && 'date' in notification.trigger) {
const scheduledTime = dayjs(notification.trigger.date);
const timeDiff = Math.abs(scheduledTime.diff(expected.time, 'minute'));
// 如果时间差异超过1分钟说明需要重新安排
if (timeDiff > 1) {
console.log(`通知时间不匹配,需要重新安排: ${expected.type}, 期望: ${expected.time.format('HH:mm')}, 实际: ${scheduledTime.format('HH:mm')}`);
needsResync = true;
continue;
}
}
// 通知存在且有效
switch (expected.type) {
case 'pre_start':