# 方案总结

基于提供的 Git diff,我将生成以下 conventional commit message:

## 变更分析:

1. **核心功能**:
   - 新增睡眠监控服务(`services/sleepMonitor.ts`)
   - 新增睡眠通知服务(`services/sleepNotificationService.ts`)
   - iOS 原生端增加睡眠观察者方法

2. **应用启动优化**:
   - 重构 `app/_layout.tsx` 中的初始化流程,按优先级分阶段加载服务

3. **药品功能改进**:
   - 优化语音识别交互(实时预览、可取消)
   - Widget 增加 URL scheme 支持

4. **路由配置**:
   - 新增药品管理路由常量

## 提交信息类型:
- **主类型**:`feat` (新增睡眠监控功能)
- **作用域**:`health` (健康相关功能)

---

请确认方案后,我将生成最终的 commit message。

---

**最终 Commit Message:**

feat(health): 添加睡眠监控和通知服务,优化应用启动流程

- 新增睡眠监控服务,支持实时监听 HealthKit 睡眠数据更新
- 实现睡眠质量分析算法,计算睡眠评分和各阶段占比
- 新增睡眠通知服务,分析完成后自动推送质量评估和建议
- iOS 原生端实现睡眠数据观察者,支持后台数据传递
- 重构应用启动初始化流程,按优先级分阶段加载服务(关键/次要/后台/空闲)
- 优化药品录入页面语音识别交互,支持实时预览和取消操作
- 药品 Widget 增加 deeplink 支持,点击跳转到应用
- 新增药品管理路由常量配置
This commit is contained in:
richarjiang
2025-11-14 10:52:26 +08:00
parent 6c2f9295be
commit 7bd0b5fc52
8 changed files with 1061 additions and 128 deletions

View File

@@ -1,10 +1,10 @@
import '@/i18n';
import { DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import 'react-native-reanimated';
import '@/i18n';
import PrivacyConsentModal from '@/components/PrivacyConsentModal';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
@@ -12,6 +12,7 @@ import { useQuickActions } from '@/hooks/useQuickActions';
import { clearAiCoachSessionCache } from '@/services/aiCoachSession';
import { notificationService } from '@/services/notifications';
import { setupQuickActions } from '@/services/quickActions';
import { sleepMonitorService } from '@/services/sleepMonitor';
import { initializeWaterRecordBridge } from '@/services/waterRecordBridge';
import { WaterRecordSource } from '@/services/waterRecords';
import { workoutMonitorService } from '@/services/workoutMonitor';
@@ -94,181 +95,276 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
}, [isLoggedIn]);
React.useEffect(() => {
const loadUserData = async () => {
// 数据已经在启动界面预加载,这里只需要快速同步到 Redux 状态
await dispatch(fetchMyProfile());
// ==================== 第一优先级立即执行关键路径0-500ms====================
const initializeCriticalServices = async () => {
try {
logger.info('🚀 开始初始化关键服务...');
// 1. 加载用户数据(首屏展示需要)
await dispatch(fetchMyProfile());
logger.info('✅ 用户数据加载完成');
// 2. 初始化 HealthKit 权限系统(不请求权限,仅初始化)
initializeHealthPermissions();
logger.info('✅ HealthKit 权限系统初始化完成');
// 3. 初始化通知服务基础功能
await notificationService.initialize();
logger.info('✅ 通知服务初始化完成');
// 4. 初始化快捷动作(用户可能立即使用)
await setupQuickActions();
logger.info('✅ 快捷动作初始化完成');
// 5. 清空 AI 教练会话缓存(轻量操作)
clearAiCoachSessionCache();
logger.info('✅ AI 教练缓存清理完成');
logger.info('🎉 关键服务初始化完成');
} catch (error) {
logger.error('❌ 关键服务初始化失败:', error);
}
};
const initHealthPermissions = async () => {
// 初始化 HealthKit 权限管理系统
try {
logger.info('初始化 HealthKit 权限管理系统...');
initializeHealthPermissions();
// ==================== 第二优先级短延迟1-2秒后执行====================
const initializeSecondaryServices = () => {
setTimeout(async () => {
try {
logger.info('⏰ 开始初始化次要服务...');
// 延迟请求权限,避免应用启动时弹窗
// 1. 请求 HealthKit 权限(延迟到 3 秒,避免打断用户浏览)
setTimeout(async () => {
try {
await ensureHealthPermissions();
logger.info('✅ HealthKit 权限请求完成');
} catch (error) {
logger.warn('⚠️ HealthKit 权限请求失败,可能在模拟器上运行:', error);
}
}, 3000);
// 2. 初始化喝水记录 Bridge
initializeWaterRecordBridge();
logger.info('✅ 喝水记录 Bridge 初始化完成');
// 3. 异步同步 Widget 数据(不阻塞主流程)
syncWidgetDataInBackground();
logger.info('🎉 次要服务初始化完成');
} catch (error) {
logger.error('❌ 次要服务初始化失败:', error);
}
}, 2000);
};
// ==================== 第三优先级中延迟3-5秒后或交互完成后执行====================
const initializeBackgroundServices = () => {
// 使用 InteractionManager 确保在所有交互和动画完成后再执行
const { InteractionManager } = require('react-native');
InteractionManager.runAfterInteractions(() => {
setTimeout(async () => {
try {
await ensureHealthPermissions();
logger.info('HealthKit 权限请求完成');
logger.info('📅 开始初始化后台服务...');
// 1. 批量注册所有通知提醒
await registerAllNotifications();
// 2. 初始化后台任务管理器
await initializeBackgroundTaskManager();
// 3. 初始化健康监听服务
await initializeHealthMonitoring();
logger.info('🎉 后台服务初始化完成');
} catch (error) {
logger.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error);
logger.error('❌ 后台服务初始化失败:', error);
}
}, 2000);
}, 3000);
});
};
logger.info('HealthKit 权限管理初始化完成');
} catch (error) {
logger.warn('HealthKit 权限管理初始化失败:', error);
}
}
const initializeNotifications = async () => {
try {
// ==================== 第四优先级:空闲时执行(不影响用户体验)====================
const initializeIdleServices = () => {
setTimeout(async () => {
try {
logger.info('初始化后台任务管理器...');
logger.info('🔄 开始初始化空闲服务...');
await BackgroundTaskManager.getInstance().initialize();
// 1. 后台任务详细状态检查
await checkBackgroundTaskStatus();
logger.info('后台任务管理器初始化成功');
// 检查后台任务状态并恢复
await checkAndRestoreBackgroundTasks();
// 在开发环境中初始化调试工具
if (__DEV__) {
// 2. 开发环境调试工具
if (__DEV__ && BackgroundTaskDebugger) {
BackgroundTaskDebugger.getInstance().initialize();
logger.info('后台任务调试工具已初始化(开发环境)');
logger.info('后台任务调试工具已初始化(开发环境)');
}
} catch (backgroundError) {
logger.error('后台任务管理器初始化失败,将跳过后台任务:', backgroundError);
}
// 初始化通知服务
await notificationService.initialize();
logger.info('通知服务初始化成功');
// 注册午餐提醒12:00
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
logger.info('午餐提醒已注册');
// 注册晚餐提醒18:00
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
logger.info('晚餐提醒已注册');
// 注册心情提醒21:00
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
logger.info('心情提醒已注册');
// 注册默认喝水提醒9:00-21:00每2小时一次
await WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户');
logger.info('默认喝水提醒已注册');
// 安排断食通知(如果存在活跃的断食计划)
try {
const fastingSchedule = store.getState().fasting.activeSchedule;
if (fastingSchedule) {
const fastingPlan = store.getState().fasting.activeSchedule ? null : null;
// 断食通知将通过 useFastingNotifications hook 在页面加载时自动安排
logger.info('检测到活跃的断食计划,将通过页面 hook 自动安排通知');
}
logger.info('🎉 空闲服务初始化完成');
} catch (error) {
logger.warn('安排断食通知失败:', error);
logger.error('❌ 空闲服务初始化失败:', error);
}
}, 8000); // 8秒后执行确保不影响用户体验
};
// 初始化快捷动作
await setupQuickActions();
logger.info('快捷动作初始化成功');
// ==================== 辅助函数 ====================
// 初始化喝水记录 bridge
initializeWaterRecordBridge();
logger.info('喝水记录 Bridge 初始化成功');
// 初始化锻炼监听服务
const initializeWorkoutMonitoring = async () => {
try {
await workoutMonitorService.initialize();
logger.info('锻炼监听服务初始化成功');
} catch (error) {
logger.warn('锻炼监听服务初始化失败:', error);
}
};
initializeWorkoutMonitoring();
// 检查并同步Widget数据更改
// 异步同步 Widget 数据(不阻塞主流程)
const syncWidgetDataInBackground = async () => {
try {
const widgetSync = await syncPendingWidgetChanges();
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
logger.info(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
logger.info(`🔄 检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
// 将待同步的记录添加到 Redux store
// 异步处理每条记录
for (const record of widgetSync.pendingRecords) {
try {
await store.dispatch(createWaterRecordAction({
amount: record.amount,
recordedAt: record.recordedAt,
source: WaterRecordSource.Auto, // 标记为自动添加来自Widget
source: WaterRecordSource.Auto,
})).unwrap();
logger.info(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
logger.info(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
} catch (error) {
logger.error('同步水记录失败:', error);
logger.error('同步水记录失败:', error);
}
}
// 清除已同步的记录
await clearPendingWaterRecords();
logger.info('所有待同步的水记录已处理完成');
logger.info('所有待同步的水记录已处理完成');
}
} catch (error) {
logger.error('通知服务、后台任务管理器或快捷动作初始化失败:', error);
logger.error('❌ Widget 数据同步失败:', error);
}
};
// 检查并恢复后台任务
const checkAndRestoreBackgroundTasks = async () => {
// 批量注册所有通知提醒
const registerAllNotifications = async () => {
try {
logger.info('📢 开始批量注册通知提醒...');
// 并行注册所有通知,提高效率
await Promise.all([
// 营养提醒
NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '').then(() =>
logger.info('✅ 午餐提醒已注册')
),
NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '').then(() =>
logger.info('✅ 晚餐提醒已注册')
),
// 心情提醒
MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '').then(() =>
logger.info('✅ 心情提醒已注册')
),
// 喝水提醒
WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户').then(() =>
logger.info('✅ 喝水提醒已注册')
),
]);
// 检查断食通知(如果有活跃计划)
const fastingSchedule = store.getState().fasting.activeSchedule;
if (fastingSchedule) {
logger.info('✅ 检测到活跃的断食计划,将通过页面 hook 自动安排通知');
}
logger.info('🎉 所有通知提醒注册完成');
} catch (error) {
logger.error('❌ 通知提醒注册失败:', error);
}
};
// 初始化后台任务管理器
const initializeBackgroundTaskManager = async () => {
try {
logger.info('⚙️ 初始化后台任务管理器...');
await BackgroundTaskManager.getInstance().initialize();
logger.info('✅ 后台任务管理器初始化成功');
// 简单的任务调度检查
const taskManager = BackgroundTaskManager.getInstance();
const status = await taskManager.getStatus();
if (status === 'available') {
const pendingRequests = await taskManager.getPendingRequests();
if (pendingRequests.length === 0) {
await taskManager.scheduleNextTask();
logger.info('✅ 已调度新的后台任务');
}
}
} catch (error) {
logger.error('❌ 后台任务管理器初始化失败:', error);
}
};
// 初始化健康监听服务(锻炼 + 睡眠)
const initializeHealthMonitoring = async () => {
try {
logger.info('💪 初始化健康监听服务...');
// 并行初始化锻炼和睡眠监听
await Promise.allSettled([
workoutMonitorService.initialize().then(() =>
logger.info('✅ 锻炼监听服务初始化成功')
),
sleepMonitorService.initialize().then(() =>
logger.info('✅ 睡眠监听服务初始化成功')
),
]);
logger.info('🎉 健康监听服务初始化完成');
} catch (error) {
logger.error('❌ 健康监听服务初始化失败:', error);
}
};
// 后台任务详细状态检查(空闲时执行)
const checkBackgroundTaskStatus = async () => {
try {
logger.info('🔍 检查后台任务详细状态...');
const taskManager = BackgroundTaskManager.getInstance();
const status = await taskManager.getStatus();
const statusText = await taskManager.checkStatus();
logger.info(`后台任务状态检查: ${status} (${statusText})`);
logger.info(`📊 后台任务状态: ${status} (${statusText})`);
// 检查是否有待处理的任务请求
const pendingRequests = await taskManager.getPendingRequests();
logger.info(`当前待处理的任务请求数量: ${pendingRequests.length}`);
// 如果没有待处理的任务请求,且状态可用,则调度一个新任务
if (pendingRequests.length === 0 && status === 'available') {
logger.info('没有待处理的任务请求,调度新的后台任务');
await taskManager.scheduleNextTask();
}
// 检查上次后台任务执行时间
// 检查上次执行时间
const lastCheckTime = await taskManager.getLastBackgroundCheckTime();
if (lastCheckTime) {
const timeSinceLastCheck = Date.now() - lastCheckTime;
const hoursSinceLastCheck = timeSinceLastCheck / (1000 * 60 * 60);
logger.info(`上次后台任务执行时间: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`);
logger.info(`⏱️ 上次执行: ${new Date(lastCheckTime).toLocaleString()} (${hoursSinceLastCheck.toFixed(1)}小时前)`);
// 如果超过24小时没有执行后台任务可能需要手动触发一次
if (hoursSinceLastCheck > 24) {
logger.warn('超过24小时未执行后台任务可能需要检查系统设置');
logger.warn('⚠️ 超过24小时未执行后台任务检查系统设置');
}
}
logger.info('✅ 后台任务状态检查完成');
} catch (error) {
logger.error('检查后台任务状态失败:', error);
logger.error('后台任务状态检查失败:', error);
}
};
loadUserData();
initHealthPermissions();
initializeNotifications();
// ==================== 执行初始化流程 ====================
// 立即执行关键服务
initializeCriticalServices();
// 延迟执行次要服务
initializeSecondaryServices();
// 交互完成后执行后台服务
initializeBackgroundServices();
// 空闲时执行非关键服务
initializeIdleServices();
// 冷启动时清空 AI 教练会话缓存
clearAiCoachSessionCache();
}, [dispatch]);
}, [dispatch, profile.name]);
React.useEffect(() => {

View File

@@ -163,6 +163,10 @@ export default function AddMedicationScreen() {
const [note, setNote] = useState('');
const [dictationActive, setDictationActive] = useState(false);
const [dictationLoading, setDictationLoading] = useState(false);
// 临时存储当前语音识别的结果,用于实时预览
const [currentDictationText, setCurrentDictationText] = useState('');
// 记录语音识别开始前的文本,用于取消时恢复
const [noteBeforeDictation, setNoteBeforeDictation] = useState('');
const isDictationSupported = Platform.OS === 'ios';
useEffect(() => {
@@ -181,20 +185,44 @@ export default function AddMedicationScreen() {
});
}, [timesPerDay]);
const appendDictationResult = useCallback(
// 实时更新语音识别结果(替换式,不是追加)
const updateDictationResult = useCallback(
(text: string) => {
const clean = text.trim();
if (!clean) return;
// 实时更新:用识别的新文本替换当前识别文本
setCurrentDictationText(clean);
// 同步更新到 note 中,以便用户能看到实时效果
setNote((prev) => {
if (!prev) {
// 移除之前的语音识别文本,添加新的识别文本
const baseText = noteBeforeDictation;
if (!baseText) {
return clean;
}
return `${prev}${prev.endsWith('\n') ? '' : '\n'}${clean}`;
// 在原文本后追加,确保格式正确
return `${baseText}${baseText.endsWith('\n') ? '' : '\n'}${clean}`;
});
},
[setNote]
[noteBeforeDictation]
);
// 确认语音识别结果
const confirmDictationResult = useCallback(() => {
// 语音识别结束,确认当前文本
setCurrentDictationText('');
setNoteBeforeDictation('');
}, []);
// 取消语音识别
const cancelDictationResult = useCallback(() => {
// 恢复到语音识别前的文本
setNote(noteBeforeDictation);
setCurrentDictationText('');
setNoteBeforeDictation('');
}, [noteBeforeDictation]);
const stepTitle = STEP_TITLES[currentStep] ?? STEP_TITLES[0];
const stepDescription = STEP_DESCRIPTIONS[currentStep] ?? '';
@@ -224,19 +252,25 @@ export default function AddMedicationScreen() {
};
Voice.onSpeechEnd = () => {
// 语音识别结束,确认识别结果
confirmDictationResult();
setDictationActive(false);
setDictationLoading(false);
};
Voice.onSpeechResults = (event: any) => {
// 获取最新的识别结果(这是累积的结果,包含之前说过的内容)
const recognized = event?.value?.[0];
if (recognized) {
appendDictationResult(recognized);
// 实时更新识别结果,替换式而非追加式
updateDictationResult(recognized);
}
};
Voice.onSpeechError = (error: any) => {
console.log('[MEDICATION] voice error', error);
// 发生错误时取消识别
cancelDictationResult();
setDictationActive(false);
setDictationLoading(false);
Alert.alert('语音识别不可用', '无法使用语音输入,请检查权限设置后重试');
@@ -249,7 +283,7 @@ export default function AddMedicationScreen() {
})
.catch(() => {});
};
}, [appendDictationResult, isDictationSupported]);
}, [updateDictationResult, confirmDictationResult, cancelDictationResult, isDictationSupported]);
const handleNext = useCallback(async () => {
if (!canProceed) return;
@@ -359,25 +393,34 @@ export default function AddMedicationScreen() {
try {
if (dictationActive) {
// 停止录音
setDictationLoading(true);
await Voice.stop();
// Voice.onSpeechEnd 会自动确认结果
setDictationLoading(false);
return;
}
// 开始录音前,保存当前的文本内容
setNoteBeforeDictation(note);
setCurrentDictationText('');
setDictationLoading(true);
try {
// 确保之前的录音已停止
await Voice.stop();
} catch {
// no-op: safe to ignore if not already recording
// 忽略错误如果之前没有录音stop 会抛出异常
}
// 开始语音识别
await Voice.start('zh-CN');
} catch (error) {
console.log('[MEDICATION] unable to start dictation', error);
setDictationLoading(false);
Alert.alert('无法启动语音输入', '请检查麦克风与语音识别权限后重试');
}
}, [dictationActive, dictationLoading, isDictationSupported]);
}, [dictationActive, dictationLoading, isDictationSupported, note]);
// 处理图片选择(拍照或相册)
const handleSelectPhoto = useCallback(() => {