feat: 添加后台任务调试工具并优化水提醒任务逻辑
This commit is contained in:
141
CLAUDE.md
141
CLAUDE.md
@@ -3,42 +3,121 @@
|
|||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
- **Run on iOS**: `npm run ios`
|
|
||||||
|
### Development
|
||||||
|
- **`npm run ios`** - Build and run on iOS Simulator (iOS-only deployment)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Automated testing is minimal; complex logic should include Jest + React Native Testing Library specs under `__tests__/` directories or alongside modules
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
- **Framework**: React Native (Expo Prebuild/Ejected) with TypeScript using Expo Router for file-based navigation
|
|
||||||
- **State Management**: Redux Toolkit with domain-specific slices (`store/`) and typed hooks (`hooks/redux.ts`)
|
### Core Stack
|
||||||
- **Authentication**: Custom auth guard system with `useAuthGuard` hook for protected navigation
|
- **Framework**: React Native (Expo Prebuild/Ejected) with TypeScript
|
||||||
- **Navigation**:
|
- **Navigation**: Expo Router with file-based routing in `app/` directory
|
||||||
- File-based routing in `app/` directory with nested layouts
|
- **State Management**: Redux Toolkit with domain-specific slices in `store/`
|
||||||
- Tab-based navigation with custom styling and haptic feedback
|
- **Styling**: Custom theme system with light/dark mode support
|
||||||
- Route constants defined in `constants/Routes.ts`, every page should use Routes define and jump
|
|
||||||
- **UI System**:
|
### Directory Structure
|
||||||
- Themed components (`ThemedText`, `ThemedView`) with color scheme support
|
- **`app/`** - Expo Router screens; tab flows in `app/(tabs)/`, feature-specific pages in nested directories
|
||||||
- Custom icon system with `IconSymbol` component for iOS symbols
|
- **`store/`** - Redux slices organized by feature (user, nutrition, workout, etc.)
|
||||||
- Reusable UI components in `components/ui/`
|
- **`services/`** - API services, backend integration, and data layer logic
|
||||||
- UI Colors in `constants/Colors.ts`
|
- **`components/`** - Reusable UI components and domain-specific components
|
||||||
- **Data Layer**:
|
- **`hooks/`** - Custom React hooks including typed Redux hooks (`hooks/redux.ts`)
|
||||||
- API services in `services/` directory with centralized API client
|
- **`utils/`** - Utility functions (health data, notifications, fasting, etc.)
|
||||||
- AsyncStorage for local persistence
|
- **`contexts/`** - React Context providers (ToastContext, MembershipModalContext)
|
||||||
- Background task management for sync operations
|
- **`constants/`** - Route definitions (`Routes.ts`), colors, and app-wide constants
|
||||||
- **Native Integration**:
|
- **`types/`** - TypeScript type definitions
|
||||||
- Health data integration with HealthKit
|
- **`assets/`** - Images, fonts, and media files
|
||||||
- Apple Authentication
|
- **`ios/`** - iOS native code and configuration
|
||||||
- Camera and photo library access for posture assessment
|
|
||||||
- Push notifications with background task support
|
### Navigation
|
||||||
- Haptic feedback integration
|
- **File-based routing**: Pages defined by file structure in `app/`
|
||||||
|
- **Tab navigation**: Main tabs in `app/(tabs)/` (Explore, Coach, Statistics, Challenges, Personal, Fasting)
|
||||||
|
- **Route constants**: All route paths defined in `constants/Routes.ts`
|
||||||
|
- **Nested layouts**: Feature-specific layouts in nested directories (e.g., `app/nutrition/_layout.tsx`)
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
- **Redux slices**: Feature-based state organization (17+ slices including user, nutrition, workout, mood, etc.)
|
||||||
|
- **Auto-sync middleware**: Listener middleware automatically syncs checkin data changes to backend
|
||||||
|
- **Typed hooks**: Use `useAppSelector` and `useAppDispatch` from `hooks/redux.ts` for type safety
|
||||||
|
- **Persistence**: AsyncStorage for local data persistence
|
||||||
|
|
||||||
|
### UI System
|
||||||
|
- **Themed components**: `ThemedText`, `ThemedView` with dynamic color scheme support
|
||||||
|
- **Custom icons**: `IconSymbol` component for iOS SF Symbols
|
||||||
|
- **UI library**: Reusable components in `components/ui/`
|
||||||
|
- **Colors**: Centralized in `constants/Colors.ts`
|
||||||
|
- **Safe areas**: `useSafeAreaTop` and `useSafeAreaWithPadding` hooks for device-safe layouts
|
||||||
|
|
||||||
|
### Data Layer
|
||||||
|
- **API client**: Centralized in `services/api.ts` with interceptors and error handling
|
||||||
|
- **Service modules**: Domain-specific services in `services/` (nutrition, workout, notifications, etc.)
|
||||||
|
- **Background tasks**: Managed by `backgroundTaskManager.ts` for sync operations
|
||||||
|
- **Local storage**: AsyncStorage for offline-first data persistence
|
||||||
|
|
||||||
|
### Native Integration
|
||||||
|
- **HealthKit**: Health data integration in `utils/health.ts` and `utils/healthKit.ts`
|
||||||
|
- **Apple Authentication**: Configured in Expo settings
|
||||||
|
- **Camera & Photos**: Food recognition and posture assessment features
|
||||||
|
- **Push Notifications**: `services/notifications.ts` with background task support
|
||||||
|
- **Haptic Feedback**: `utils/haptics.ts` for user interactions
|
||||||
|
- **Quick Actions**: Expo quick actions integration
|
||||||
|
|
||||||
|
### Context Providers
|
||||||
|
- **ToastContext** - Global toast notification system
|
||||||
|
- **MembershipModalContext** - VIP membership feature access control
|
||||||
|
|
||||||
## Key Architecture Patterns
|
## Key Architecture Patterns
|
||||||
- **Redux Auto-sync**: Listener middleware automatically syncs checkin data changes to backend
|
|
||||||
- **Type-safe Navigation**: Uses Expo Router with TypeScript for route type safety
|
- **Redux Auto-sync**: Listener middleware in `store/index.ts` automatically syncs checkin data changes to backend
|
||||||
|
- **Type-safe Navigation**: Expo Router with TypeScript for compile-time route safety
|
||||||
- **Authentication Flow**: `pushIfAuthedElseLogin` function handles auth-protected navigation
|
- **Authentication Flow**: `pushIfAuthedElseLogin` function handles auth-protected navigation
|
||||||
- **Theme System**: Dynamic theming with light/dark mode support and color tokens
|
- **Theme System**: Dynamic theming with light/dark mode and color tokens
|
||||||
- **Service Layer**: Centralized API client with interceptors and error handling
|
- **Service Layer**: Centralized API client with interceptors and error handling
|
||||||
|
- **Background Sync**: Automatic data synchronization via background task manager
|
||||||
|
|
||||||
## Development Conventions
|
## Development Conventions
|
||||||
- Use absolute imports with `@/` prefix for all internal imports
|
|
||||||
- Follow existing Redux slice patterns for state management
|
### Import Patterns
|
||||||
- Implement auth guards using `useAuthGuard` hook for protected features
|
- **Absolute imports**: Use `@/` prefix for all internal imports (e.g., `@/store`, `@/services/api`)
|
||||||
- Use themed components for consistent styling
|
- **Alias configuration**: Defined in `tsconfig.json` paths
|
||||||
- Follow established navigation patterns with typed routes
|
|
||||||
|
### Redux Patterns
|
||||||
|
- **Feature slices**: Each feature has its own slice (userSlice.ts, nutritionSlice.ts, etc.)
|
||||||
|
- **Typed hooks**: Always use `useAppSelector` and `useAppDispatch` from `hooks/redux.ts`
|
||||||
|
- **Async actions**: Use Redux Toolkit thunks for async operations
|
||||||
|
- **Auto-sync**: Listener middleware handles automatic data synchronization
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- **Components**: PascalCase (e.g., `ThemedText.tsx`, `FitnessRingsCard.tsx`)
|
||||||
|
- **Hooks**: camelCase with "use" prefix (e.g., `useAppSelector`, `useSafeAreaTop`)
|
||||||
|
- **Utilities**: camelCase (e.g., `health.ts`, `notificationHelpers.ts`)
|
||||||
|
- **Screen files**: kebab-case (e.g., `ai-posture-assessment.tsx`, `nutrition-label-analysis.tsx`)
|
||||||
|
- **Constants**: UPPER_SNAKE_CASE for values, PascalCase for types
|
||||||
|
|
||||||
|
### Code Style
|
||||||
|
- **ESLint**: Configured with `eslint-config-expo` in `eslint.config.js`
|
||||||
|
- **Formatting**: 2 spaces, trailing commas, single quotes (Prettier defaults)
|
||||||
|
- **TypeScript**: Strict mode enabled, use proper type annotations
|
||||||
|
|
||||||
|
### Navigation & Routing
|
||||||
|
- **Route constants**: Always use constants from `constants/Routes.ts` for navigation
|
||||||
|
- **Auth guards**: Implement using `useAuthGuard` hook for protected features
|
||||||
|
- **Typed routes**: Leverage Expo Router's TypeScript integration
|
||||||
|
|
||||||
|
### Testing Guidelines
|
||||||
|
- **Minimal automated tests**: Add Jest + React Native Testing Library for complex logic
|
||||||
|
- **HealthKit testing**: Requires real device; verify on iOS Simulator when possible
|
||||||
|
- **Integration tests**: Include reproduction steps and logs in PR descriptions
|
||||||
|
|
||||||
|
### iOS Development
|
||||||
|
- **Native changes**: Update `ios/` directory and re-run `npm run ios` after modifying Swift or entitlements
|
||||||
|
- **HealthKit**: Requires entitlements configuration; coordinate with release engineering
|
||||||
|
- **App signing**: Keep bundle IDs consistent with `app.json` and iOS project configuration
|
||||||
|
- **App Groups**: Required for widget and quick actions integration
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
- **Conventional Commits**: Use `feat`, `fix`, `chore` prefixes with optional scope
|
||||||
|
- **PR descriptions**: Include problem, solution, test evidence (screenshots, commands), iOS setup notes
|
||||||
|
- **Change grouping**: Group related changes; avoid bundling unrelated features
|
||||||
4
app.json
4
app.json
@@ -21,6 +21,10 @@
|
|||||||
"NSMicrophoneUsageDescription": "应用需要使用麦克风进行语音识别,将您的语音转换为文字记录饮食信息。",
|
"NSMicrophoneUsageDescription": "应用需要使用麦克风进行语音识别,将您的语音转换为文字记录饮食信息。",
|
||||||
"NSSpeechRecognitionUsageDescription": "应用需要使用语音识别功能来转换您的语音为文字,帮助您快速记录饮食信息。",
|
"NSSpeechRecognitionUsageDescription": "应用需要使用语音识别功能来转换您的语音为文字,帮助您快速记录饮食信息。",
|
||||||
"NSUserNotificationsUsageDescription": "应用需要发送通知以提醒您喝水和站立活动。",
|
"NSUserNotificationsUsageDescription": "应用需要发送通知以提醒您喝水和站立活动。",
|
||||||
|
"BGTaskSchedulerPermittedIdentifiers": [
|
||||||
|
"com.expo.modules.backgroundtask.processing",
|
||||||
|
"com.anonymous.digitalpilates.task"
|
||||||
|
],
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
"processing",
|
"processing",
|
||||||
"fetch",
|
"fetch",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
|||||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||||
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
import { loadActiveFastingSchedule } from '@/utils/fasting';
|
||||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||||
import { MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
import { MoodNotificationHelpers, NutritionNotificationHelpers, WaterNotificationHelpers } from '@/utils/notificationHelpers';
|
||||||
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
import { clearPendingWaterRecords, syncPendingWidgetChanges } from '@/utils/widgetDataSync';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
@@ -32,8 +32,20 @@ import { STORAGE_KEYS } from '@/services/api';
|
|||||||
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
import { BackgroundTaskManager } from '@/services/backgroundTaskManager';
|
||||||
import { fetchChallenges } from '@/store/challengesSlice';
|
import { fetchChallenges } from '@/store/challengesSlice';
|
||||||
import AsyncStorage from '@/utils/kvStore';
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
import { logger } from '@/utils/logger';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
// 在开发环境中导入调试工具
|
||||||
|
let BackgroundTaskDebugger: any = null;
|
||||||
|
if (__DEV__) {
|
||||||
|
try {
|
||||||
|
const debuggerModule = require('@/services/backgroundTaskDebugger');
|
||||||
|
BackgroundTaskDebugger = debuggerModule.BackgroundTaskDebugger;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('无法导入后台任务调试工具:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
function Bootstrapper({ children }: { children: React.ReactNode }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -63,7 +75,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
if (store.getState().fasting.activeSchedule) return;
|
if (store.getState().fasting.activeSchedule) return;
|
||||||
dispatch(hydrateActiveSchedule(stored));
|
dispatch(hydrateActiveSchedule(stored));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('恢复断食计划失败:', error);
|
logger.warn('恢复断食计划失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,60 +101,75 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
const initHealthPermissions = async () => {
|
const initHealthPermissions = async () => {
|
||||||
// 初始化 HealthKit 权限管理系统
|
// 初始化 HealthKit 权限管理系统
|
||||||
try {
|
try {
|
||||||
console.log('初始化 HealthKit 权限管理系统...');
|
logger.info('初始化 HealthKit 权限管理系统...');
|
||||||
initializeHealthPermissions();
|
initializeHealthPermissions();
|
||||||
|
|
||||||
// 延迟请求权限,避免应用启动时弹窗
|
// 延迟请求权限,避免应用启动时弹窗
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await ensureHealthPermissions();
|
await ensureHealthPermissions();
|
||||||
console.log('HealthKit 权限请求完成');
|
logger.info('HealthKit 权限请求完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error);
|
logger.warn('HealthKit 权限请求失败,可能在模拟器上运行:', error);
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
console.log('HealthKit 权限管理初始化完成');
|
logger.info('HealthKit 权限管理初始化完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('HealthKit 权限管理初始化失败:', error);
|
logger.warn('HealthKit 权限管理初始化失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initializeNotifications = async () => {
|
const initializeNotifications = async () => {
|
||||||
try {
|
try {
|
||||||
await BackgroundTaskManager.getInstance().initialize();
|
try {
|
||||||
|
await BackgroundTaskManager.getInstance().initialize();
|
||||||
|
|
||||||
|
// 在开发环境中初始化调试工具
|
||||||
|
if (__DEV__) {
|
||||||
|
BackgroundTaskDebugger.getInstance().initialize();
|
||||||
|
logger.info('后台任务调试工具已初始化(开发环境)');
|
||||||
|
}
|
||||||
|
} catch (backgroundError) {
|
||||||
|
logger.error('后台任务管理器初始化失败,将跳过后台任务:', backgroundError);
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化通知服务
|
// 初始化通知服务
|
||||||
await notificationService.initialize();
|
await notificationService.initialize();
|
||||||
console.log('通知服务初始化成功');
|
logger.info('通知服务初始化成功');
|
||||||
|
|
||||||
// 注册午餐提醒(12:00)
|
// 注册午餐提醒(12:00)
|
||||||
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
|
await NutritionNotificationHelpers.scheduleDailyLunchReminder(profile.name || '');
|
||||||
console.log('午餐提醒已注册');
|
logger.info('午餐提醒已注册');
|
||||||
|
|
||||||
// 注册晚餐提醒(18:00)
|
// 注册晚餐提醒(18:00)
|
||||||
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
|
await NutritionNotificationHelpers.scheduleDailyDinnerReminder(profile.name || '');
|
||||||
console.log('晚餐提醒已注册');
|
logger.info('晚餐提醒已注册');
|
||||||
|
|
||||||
// 注册心情提醒(21:00)
|
// 注册心情提醒(21:00)
|
||||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
|
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
|
||||||
console.log('心情提醒已注册');
|
logger.info('心情提醒已注册');
|
||||||
|
|
||||||
|
// 注册默认喝水提醒(9:00-21:00,每2小时一次)
|
||||||
|
await WaterNotificationHelpers.scheduleRegularWaterReminders(profile.name || '用户');
|
||||||
|
logger.info('默认喝水提醒已注册');
|
||||||
|
|
||||||
|
|
||||||
// 初始化快捷动作
|
// 初始化快捷动作
|
||||||
await setupQuickActions();
|
await setupQuickActions();
|
||||||
console.log('快捷动作初始化成功');
|
logger.info('快捷动作初始化成功');
|
||||||
|
|
||||||
// 初始化喝水记录 bridge
|
// 初始化喝水记录 bridge
|
||||||
initializeWaterRecordBridge();
|
initializeWaterRecordBridge();
|
||||||
console.log('喝水记录 Bridge 初始化成功');
|
logger.info('喝水记录 Bridge 初始化成功');
|
||||||
|
|
||||||
// 初始化锻炼监听服务
|
// 初始化锻炼监听服务
|
||||||
const initializeWorkoutMonitoring = async () => {
|
const initializeWorkoutMonitoring = async () => {
|
||||||
try {
|
try {
|
||||||
await workoutMonitorService.initialize();
|
await workoutMonitorService.initialize();
|
||||||
console.log('锻炼监听服务初始化成功');
|
logger.info('锻炼监听服务初始化成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('锻炼监听服务初始化失败:', error);
|
logger.warn('锻炼监听服务初始化失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +178,7 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
// 检查并同步Widget数据更改
|
// 检查并同步Widget数据更改
|
||||||
const widgetSync = await syncPendingWidgetChanges();
|
const widgetSync = await syncPendingWidgetChanges();
|
||||||
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
|
if (widgetSync.hasPendingChanges && widgetSync.pendingRecords) {
|
||||||
console.log(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
|
logger.info(`检测到 ${widgetSync.pendingRecords.length} 条待同步的水记录`);
|
||||||
|
|
||||||
// 将待同步的记录添加到 Redux store
|
// 将待同步的记录添加到 Redux store
|
||||||
for (const record of widgetSync.pendingRecords) {
|
for (const record of widgetSync.pendingRecords) {
|
||||||
@@ -162,18 +189,18 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
source: WaterRecordSource.Auto, // 标记为自动添加(来自Widget)
|
source: WaterRecordSource.Auto, // 标记为自动添加(来自Widget)
|
||||||
})).unwrap();
|
})).unwrap();
|
||||||
|
|
||||||
console.log(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
|
logger.info(`成功同步水记录: ${record.amount}ml at ${record.recordedAt}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('同步水记录失败:', error);
|
logger.error('同步水记录失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除已同步的记录
|
// 清除已同步的记录
|
||||||
await clearPendingWaterRecords();
|
await clearPendingWaterRecords();
|
||||||
console.log('所有待同步的水记录已处理完成');
|
logger.info('所有待同步的水记录已处理完成');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('通知服务、后台任务管理器或快捷动作初始化失败:', error);
|
logger.error('通知服务、后台任务管理器或快捷动作初始化失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>com.expo.modules.backgroundtask.processing</string>
|
<string>com.expo.modules.backgroundtask.processing</string>
|
||||||
|
<string>com.anonymous.digitalpilates.task</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -25,7 +26,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.21</string>
|
<string>1.0.22</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -81,6 +82,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>remote-notification</string>
|
<string>remote-notification</string>
|
||||||
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>SplashScreen</string>
|
<string>SplashScreen</string>
|
||||||
|
|||||||
177
services/backgroundTaskDebugger.ts
Normal file
177
services/backgroundTaskDebugger.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import AsyncStorage from '@/utils/kvStore';
|
||||||
|
import { BackgroundTaskManager } from './backgroundTaskManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台任务调试工具
|
||||||
|
* 提供简单的调试和测试功能
|
||||||
|
*/
|
||||||
|
export class BackgroundTaskDebugger {
|
||||||
|
private static instance: BackgroundTaskDebugger;
|
||||||
|
|
||||||
|
static getInstance(): BackgroundTaskDebugger {
|
||||||
|
if (!BackgroundTaskDebugger.instance) {
|
||||||
|
BackgroundTaskDebugger.instance = new BackgroundTaskDebugger();
|
||||||
|
}
|
||||||
|
return BackgroundTaskDebugger.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后台任务诊断信息
|
||||||
|
*/
|
||||||
|
async getDiagnosticInfo(): Promise<{
|
||||||
|
taskManager: any;
|
||||||
|
storage: any;
|
||||||
|
}> {
|
||||||
|
const taskManager = BackgroundTaskManager.getInstance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
taskManager: {
|
||||||
|
isInitialized: await this.getTaskManagerInitStatus(),
|
||||||
|
status: await taskManager.getStatus(),
|
||||||
|
statusText: await taskManager.checkStatus(),
|
||||||
|
},
|
||||||
|
storage: await this.getRelevantStorageValues(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动测试后台任务
|
||||||
|
*/
|
||||||
|
async testBackgroundTask(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
executionTime: number;
|
||||||
|
}> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const taskManager = BackgroundTaskManager.getInstance();
|
||||||
|
await taskManager.testBackgroundTask();
|
||||||
|
|
||||||
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
executionTime
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const executionTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
executionTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理后台任务相关数据
|
||||||
|
*/
|
||||||
|
async clearBackgroundTaskData(): Promise<void> {
|
||||||
|
const keys = [
|
||||||
|
'@last_background_water_check',
|
||||||
|
'@last_background_test_notification',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.removeItem(key);
|
||||||
|
console.log(`已清理存储键: ${key}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`清理存储键失败 ${key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置后台任务管理器
|
||||||
|
*/
|
||||||
|
async resetBackgroundTaskManager(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const taskManager = BackgroundTaskManager.getInstance();
|
||||||
|
await taskManager.stop();
|
||||||
|
await this.clearBackgroundTaskData();
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
await taskManager.initialize();
|
||||||
|
|
||||||
|
console.log('后台任务管理器已重置');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置后台任务管理器失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用测试通知
|
||||||
|
*/
|
||||||
|
async enableTestNotification(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem('@enable_test_notification', 'true');
|
||||||
|
console.log('已启用测试通知');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启用测试通知失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用测试通知
|
||||||
|
*/
|
||||||
|
async disableTestNotification(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.removeItem('@enable_test_notification');
|
||||||
|
console.log('已禁用测试通知');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('禁用测试通知失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否启用了测试通知
|
||||||
|
*/
|
||||||
|
async isTestNotificationEnabled(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const enabled = await AsyncStorage.getItem('@enable_test_notification');
|
||||||
|
return enabled === 'true';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查测试通知状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTaskManagerInitStatus(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const taskManager = BackgroundTaskManager.getInstance();
|
||||||
|
const status = await taskManager.getStatus();
|
||||||
|
const statusText = await taskManager.checkStatus();
|
||||||
|
return statusText !== '受限制';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务管理器状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getRelevantStorageValues(): Promise<Record<string, any>> {
|
||||||
|
const keys = [
|
||||||
|
'@last_background_water_check',
|
||||||
|
'@last_background_test_notification',
|
||||||
|
'@enable_test_notification',
|
||||||
|
];
|
||||||
|
|
||||||
|
const values: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
try {
|
||||||
|
const value = await AsyncStorage.getItem(key);
|
||||||
|
values[key] = value;
|
||||||
|
} catch (error) {
|
||||||
|
values[key] = `Error: ${error}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backgroundTaskDebugger = BackgroundTaskDebugger.getInstance();
|
||||||
@@ -31,10 +31,25 @@ async function executeWaterReminderTask(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
console.log('执行喝水提醒后台任务...');
|
console.log('执行喝水提醒后台任务...');
|
||||||
|
|
||||||
// 获取当前状态
|
// 获取当前状态,添加错误处理
|
||||||
const state = store.getState();
|
let state;
|
||||||
const waterStats = state.water.todayStats;
|
try {
|
||||||
const userProfile = state.user.profile;
|
state = store.getState();
|
||||||
|
} catch (error) {
|
||||||
|
console.log('无法获取 Redux state,使用本地存储:', error);
|
||||||
|
// 使用本地存储作为后备方案
|
||||||
|
const dailyGoal = await getWaterGoalFromStorage();
|
||||||
|
if (!dailyGoal || dailyGoal <= 0) {
|
||||||
|
console.log('没有设置喝水目标,跳过喝水提醒');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 简化的提醒逻辑
|
||||||
|
await sendSimpleWaterReminder();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const waterStats = state.water?.todayStats;
|
||||||
|
const userProfile = state.user?.profile;
|
||||||
|
|
||||||
// 优先使用 Redux 中的目标,若无则读取本地存储
|
// 优先使用 Redux 中的目标,若无则读取本地存储
|
||||||
let dailyGoal = waterStats?.dailyGoal ?? 0;
|
let dailyGoal = waterStats?.dailyGoal ?? 0;
|
||||||
@@ -111,9 +126,14 @@ async function executeChallengeReminderTask(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
console.log('执行挑战鼓励提醒后台任务...');
|
console.log('执行挑战鼓励提醒后台任务...');
|
||||||
|
|
||||||
const state = store.getState();
|
let userName = '朋友';
|
||||||
const normalizedUserName = state.user.profile?.name?.trim();
|
try {
|
||||||
const userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友';
|
const state = store.getState();
|
||||||
|
const normalizedUserName = state.user?.profile?.name?.trim();
|
||||||
|
userName = normalizedUserName && normalizedUserName.length > 0 ? normalizedUserName : '朋友';
|
||||||
|
} catch (error) {
|
||||||
|
console.log('无法获取用户名,使用默认值:', error);
|
||||||
|
}
|
||||||
|
|
||||||
const challenges = await listChallenges();
|
const challenges = await listChallenges();
|
||||||
const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress);
|
const joinedChallenges = challenges.filter((challenge) => challenge.isJoined && challenge.progress);
|
||||||
@@ -201,6 +221,33 @@ async function sendTestNotification(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送简单的喝水提醒(后备方案)
|
||||||
|
*/
|
||||||
|
async function sendSimpleWaterReminder(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const userName = '朋友'; // 默认用户名
|
||||||
|
const Notifications = await import('expo-notifications');
|
||||||
|
|
||||||
|
const notificationId = await Notifications.scheduleNotificationAsync({
|
||||||
|
content: {
|
||||||
|
title: '💧 该喝水啦!',
|
||||||
|
body: `${userName},记得补充水分,保持身体健康~`,
|
||||||
|
data: {
|
||||||
|
type: 'water_reminder',
|
||||||
|
url: '/statistics'
|
||||||
|
},
|
||||||
|
sound: 'default',
|
||||||
|
},
|
||||||
|
trigger: null, // 立即发送
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('简单喝水提醒已发送,ID:', notificationId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送简单喝水提醒失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 后台任务执行函数
|
// 后台任务执行函数
|
||||||
async function executeBackgroundTasks(): Promise<void> {
|
async function executeBackgroundTasks(): Promise<void> {
|
||||||
console.log('开始执行后台任务...');
|
console.log('开始执行后台任务...');
|
||||||
@@ -213,14 +260,30 @@ async function executeBackgroundTasks(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// await sendTestNotification()
|
// 确保 Redux store 已初始化
|
||||||
|
try {
|
||||||
|
const state = store.getState();
|
||||||
|
if (!state) {
|
||||||
|
console.log('Redux store 未初始化,跳过后台任务');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('无法访问 Redux store,跳过后台任务:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 执行喝水提醒检查任务 - 已禁用,改为由用户手动在设置页面管理
|
// await sendTestNotification() // 可选:启用测试通知
|
||||||
|
|
||||||
|
// 检查是否启用测试通知
|
||||||
|
const testNotificationsEnabled = await AsyncStorage.getItem('@background_test_notifications_enabled') === 'true';
|
||||||
|
if (testNotificationsEnabled) {
|
||||||
|
await sendTestNotification();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行喝水提醒检查任务
|
||||||
await executeWaterReminderTask();
|
await executeWaterReminderTask();
|
||||||
|
|
||||||
// 执行站立提醒检查任务
|
// 执行挑战鼓励提醒任务
|
||||||
// await executeStandReminderTask();
|
|
||||||
|
|
||||||
await executeChallengeReminderTask();
|
await executeChallengeReminderTask();
|
||||||
|
|
||||||
console.log('后台任务执行完成');
|
console.log('后台任务执行完成');
|
||||||
@@ -269,13 +332,15 @@ export class BackgroundTaskManager {
|
|||||||
|
|
||||||
if (await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_IDENTIFIER)) {
|
if (await TaskManager.isTaskRegisteredAsync(BACKGROUND_TASK_IDENTIFIER)) {
|
||||||
log.info('[BackgroundTask] 任务已注册');
|
log.info('[BackgroundTask] 任务已注册');
|
||||||
return
|
} else {
|
||||||
|
log.info('[BackgroundTask] 任务未注册,开始注册...');
|
||||||
|
await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER);
|
||||||
|
log.info('[BackgroundTask] 任务注册完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info('[BackgroundTask] 任务未注册, 开始注册...');
|
// 验证任务状态
|
||||||
// 注册后台任务
|
const status = await BackgroundTask.getStatusAsync();
|
||||||
await BackgroundTask.registerTaskAsync(BACKGROUND_TASK_IDENTIFIER);
|
log.info(`[BackgroundTask] 任务状态: ${status}`);
|
||||||
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
log.info('后台任务管理器初始化完成');
|
log.info('后台任务管理器初始化完成');
|
||||||
|
|||||||
Reference in New Issue
Block a user