feat: 更新健康数据权限描述,添加HRV数据获取测试功能,优化后台任务配置,调整压力计显示单位

This commit is contained in:
2025-09-06 16:34:56 +08:00
parent 2df747109c
commit 2e7daae519
7 changed files with 107 additions and 20 deletions

View File

@@ -17,8 +17,11 @@
"NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。", "NSCameraUsageDescription": "应用需要使用相机以拍摄您的体态照片用于AI测评。",
"NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。", "NSPhotoLibraryUsageDescription": "应用需要访问相册以选择您的体态照片用于AI测评。",
"NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。", "NSPhotoLibraryAddUsageDescription": "应用需要写入相册以保存拍摄的体态照片(可选)。",
"NSUserNotificationsUsageDescription": "应用需要发送通知以提醒您喝水和站立活动。",
"UIBackgroundModes": [ "UIBackgroundModes": [
"processing" "processing",
"fetch",
"remote-notification"
] ]
} }
}, },
@@ -55,7 +58,7 @@
"react-native-health", "react-native-health",
{ {
"enableHealthAPI": true, "enableHealthAPI": true,
"healthSharePermission": "应用需要访问您的健康数据(步数能量消耗)以展示运动统计。", "healthSharePermission": "应用需要访问您的健康数据(步数能量消耗、心率变异性等)以展示运动统计和压力分析。",
"healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。" "healthUpdatePermission": "应用需要更新您的健康数据(体重信息)以记录您的健身进度。"
} }
], ],

View File

@@ -17,7 +17,7 @@ import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/mo
import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice'; import { fetchDailyNutritionData, selectNutritionSummaryByDate } from '@/store/nutritionSlice';
import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice'; import { fetchTodayWaterStats, selectTodayStats } from '@/store/waterSlice';
import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date';
import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health'; import { ensureHealthPermissions, fetchHealthDataForDate, testHRVDataFetch } from '@/utils/health';
import { getTestHealthData } from '@/utils/mockHealthData'; import { getTestHealthData } from '@/utils/mockHealthData';
import { calculateNutritionGoals } from '@/utils/nutrition'; import { calculateNutritionGoals } from '@/utils/nutrition';
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
@@ -460,15 +460,27 @@ export default function ExploreScreen() {
{/* 开发环境调试按钮 */} {/* 开发环境调试按钮 */}
{__DEV__ && ( {__DEV__ && (
<TouchableOpacity <View style={styles.debugButtonsContainer}>
style={styles.debugButton} <TouchableOpacity
onPress={async () => { style={styles.debugButton}
console.log('🔧 手动触发后台任务测试...'); onPress={async () => {
// await backgroundTaskManager.triggerTaskForTesting(); console.log('🔧 手动触发后台任务测试...');
}} // await backgroundTaskManager.triggerTaskForTesting();
> }}
<Text style={styles.debugButtonText}>🔧</Text> >
</TouchableOpacity> <Text style={styles.debugButtonText}>🔧</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.debugButton, styles.hrvTestButton]}
onPress={async () => {
console.log('🫀 测试HRV数据获取...');
await testHRVDataFetch();
}}
>
<Text style={styles.debugButtonText}>🫀</Text>
</TouchableOpacity>
</View>
)} )}
</View> </View>
</View> </View>
@@ -665,6 +677,10 @@ const styles = StyleSheet.create({
fontWeight: '500', fontWeight: '500',
color: '#192126', color: '#192126',
}, },
debugButtonsContainer: {
flexDirection: 'row',
gap: 8,
},
debugButton: { debugButton: {
width: 32, width: 32,
height: 32, height: 32,
@@ -681,6 +697,9 @@ const styles = StyleSheet.create({
shadowRadius: 4, shadowRadius: 4,
elevation: 3, elevation: 3,
}, },
hrvTestButton: {
backgroundColor: '#8B5CF6',
},
debugButtonText: { debugButtonText: {
fontSize: 12, fontSize: 12,
}, },

View File

@@ -82,7 +82,7 @@ export function StressMeter({ value, updateTime, style, hrvValue }: StressMeterP
{/* 数值显示区域 */} {/* 数值显示区域 */}
<View style={styles.valueSection}> <View style={styles.valueSection}>
<Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text> <Text style={styles.value}>{stressIndex === null ? '--' : stressIndex}</Text>
<Text style={styles.unit}>ms</Text> <Text style={styles.unit}>%</Text>
</View> </View>
{/* 进度条区域 */} {/* 进度条区域 */}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View File

@@ -4,7 +4,7 @@
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.anonymous.digitalpilates.backgroundfetch</string> <string>background-health-reminders</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
@@ -23,7 +23,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.7</string> <string>1.0.8</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@@ -54,13 +54,15 @@
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>应用需要使用相机以实现相关功能</string> <string>应用需要使用相机以实现相关功能</string>
<key>NSHealthShareUsageDescription</key> <key>NSHealthShareUsageDescription</key>
<string>应用需要访问您的健康数据(步数能量消耗)以展示运动统计。</string> <string>应用需要访问您的健康数据(步数能量消耗、心率变异性等)以展示运动统计和压力分析。</string>
<key>NSHealthUpdateUsageDescription</key> <key>NSHealthUpdateUsageDescription</key>
<string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string> <string>应用需要更新您的健康数据(体重信息)以记录您的健身进度。</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>应用需要写入相册以保存拍摄的体态照片(可选)。</string> <string>应用需要写入相册以保存拍摄的体态照片(可选)。</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>应用需要访问相册以选择您的体态照片用于AI测评。</string> <string>应用需要访问相册以选择您的体态照片用于AI测评。</string>
<key>NSUserNotificationsUsageDescription</key>
<string>应用需要发送通知以提醒您喝水和站立活动。</string>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
@@ -68,7 +70,8 @@
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>processing</string> <string>processing</string>
<string>fetch</string> <string>fetch</string>
<string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>SplashScreen</string> <string>SplashScreen</string>

View File

@@ -3,6 +3,7 @@ import { StandReminderHelpers, WaterNotificationHelpers } from '@/utils/notifica
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import * as BackgroundTask from 'expo-background-task'; import * as BackgroundTask from 'expo-background-task';
import * as TaskManager from 'expo-task-manager'; import * as TaskManager from 'expo-task-manager';
import { TaskManagerTaskBody } from 'expo-task-manager';
/** /**
* 后台任务标识符 * 后台任务标识符
@@ -14,7 +15,7 @@ export const BACKGROUND_TASK_IDS = {
} as const; } as const;
// 定义后台任务 // 定义后台任务
TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async () => { TaskManager.defineTask(BACKGROUND_TASK_IDS.HEALTH_REMINDERS, async (body: TaskManagerTaskBody) => {
try { try {
console.log('[BackgroundTask] 后台任务执行'); console.log('[BackgroundTask] 后台任务执行');
await executeBackgroundTasks(); await executeBackgroundTasks();

View File

@@ -452,22 +452,43 @@ async function fetchSleepDuration(options: HealthDataOptions): Promise<number> {
async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> { async function fetchHeartRateVariability(options: HealthDataOptions): Promise<number | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
console.log('=== 开始获取HRV数据 ===');
console.log('查询选项:', options);
AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => { AppleHealthKit.getHeartRateVariabilitySamples(options, (err, res) => {
console.log('HRV API调用结果:', { err, res });
if (err) { if (err) {
logError('HRV数据', err); logError('HRV数据', err);
console.error('HRV获取错误详情:', err);
return resolve(null); return resolve(null);
} }
if (!res || !Array.isArray(res) || res.length === 0) { if (!res || !Array.isArray(res) || res.length === 0) {
logWarning('HRV', '为空或格式错误'); logWarning('HRV', '为空或格式错误');
console.warn('HRV数据为空原始响应:', res);
return resolve(null); return resolve(null);
} }
logSuccess('HRV', res); logSuccess('HRV', res);
console.log('HRV数据样本数量:', res.length);
// 打印最新的几个样本用于调试
const latestSamples = res.slice(-3);
console.log('最新的HRV样本:', latestSamples.map(sample => ({
value: sample.value,
startDate: sample.startDate,
endDate: sample.endDate
})));
const latestHrv = res[res.length - 1]; const latestHrv = res[res.length - 1];
if (latestHrv && latestHrv.value) { if (latestHrv && latestHrv.value !== undefined && latestHrv.value !== null) {
// HealthKit 中的 HRV 数据已经是毫秒单位,无需转换 // HealthKit 中的 HRV 数据已经是毫秒单位,无需转换
resolve(Math.round(latestHrv.value)); const hrvValue = Math.round(latestHrv.value);
console.log('最终HRV值:', hrvValue);
resolve(hrvValue);
} else { } else {
console.warn('HRV样本值无效:', latestHrv);
resolve(null); resolve(null);
} }
}); });
@@ -681,6 +702,41 @@ export async function fetchRecentHRV(hoursBack: number = 2): Promise<number | nu
return fetchHeartRateVariability(options); return fetchHeartRateVariability(options);
} }
// 测试HRV数据获取功能
export async function testHRVDataFetch(date: Date = dayjs().toDate()): Promise<void> {
console.log('=== 开始测试HRV数据获取 ===');
try {
// 首先确保权限
const hasPermission = await ensureHealthPermissions();
if (!hasPermission) {
console.error('没有健康数据权限无法测试HRV');
return;
}
console.log('权限检查通过开始获取HRV数据...');
// 测试不同时间范围的HRV数据
const options = createDateRange(date);
// 获取今日HRV
const todayHRV = await fetchHeartRateVariability(options);
console.log('今日HRV结果:', todayHRV);
// 获取最近2小时HRV
const recentHRV = await fetchRecentHRV(2);
console.log('最近2小时HRV结果:', recentHRV);
// 获取指定日期HRV
const dateHRV = await fetchHRVForDate(date);
console.log('指定日期HRV结果:', dateHRV);
console.log('=== HRV数据测试完成 ===');
} catch (error) {
console.error('HRV测试过程中出现错误:', error);
}
}
// 更新healthkit中的体重 // 更新healthkit中的体重
export async function updateWeight(weight: number) { export async function updateWeight(weight: number) {
return new Promise((resolve) => { return new Promise((resolve) => {