feat(workout): 优化心率图表性能并移除每日总结通知功能
- 重构心率数据采样算法,采用智能采样保留峰值、谷值和变化率大的点 - 减少心率图表最大数据点数和查询限制,提升渲染性能 - 移除图表背景线样式,简化视觉呈现 - 完全移除每日总结通知功能相关代码和调用
This commit is contained in:
@@ -17,7 +17,7 @@ import { store } from '@/store';
|
|||||||
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
import { fetchMyProfile, setPrivacyAgreed } from '@/store/userSlice';
|
||||||
import { createWaterRecordAction } from '@/store/waterSlice';
|
import { createWaterRecordAction } from '@/store/waterSlice';
|
||||||
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
import { ensureHealthPermissions, initializeHealthPermissions } from '@/utils/health';
|
||||||
import { DailySummaryNotificationHelpers, MoodNotificationHelpers, NutritionNotificationHelpers } from '@/utils/notificationHelpers';
|
import { MoodNotificationHelpers, NutritionNotificationHelpers } 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';
|
||||||
|
|
||||||
@@ -93,8 +93,6 @@ function Bootstrapper({ children }: { children: React.ReactNode }) {
|
|||||||
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
|
await MoodNotificationHelpers.scheduleDailyMoodReminder(profile.name || '');
|
||||||
console.log('心情提醒已注册');
|
console.log('心情提醒已注册');
|
||||||
|
|
||||||
await DailySummaryNotificationHelpers.scheduleDailySummaryNotification(profile.name || '')
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化快捷动作
|
// 初始化快捷动作
|
||||||
await setupQuickActions();
|
await setupQuickActions();
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ interface WorkoutDetailModalProps {
|
|||||||
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
||||||
const SHEET_MAX_HEIGHT = SCREEN_HEIGHT * 0.9;
|
const SHEET_MAX_HEIGHT = SCREEN_HEIGHT * 0.9;
|
||||||
|
|
||||||
const HEART_RATE_CHART_MAX_POINTS = 120;
|
const HEART_RATE_CHART_MAX_POINTS = 80;
|
||||||
|
|
||||||
export function WorkoutDetailModal({
|
export function WorkoutDetailModal({
|
||||||
visible,
|
visible,
|
||||||
@@ -338,7 +338,7 @@ export function WorkoutDetailModal({
|
|||||||
height={220}
|
height={220}
|
||||||
fromZero={false}
|
fromZero={false}
|
||||||
yAxisSuffix="次/分"
|
yAxisSuffix="次/分"
|
||||||
withInnerLines
|
withInnerLines={false}
|
||||||
bezier
|
bezier
|
||||||
chartConfig={{
|
chartConfig={{
|
||||||
backgroundColor: '#FFFFFF',
|
backgroundColor: '#FFFFFF',
|
||||||
@@ -352,11 +352,6 @@ export function WorkoutDetailModal({
|
|||||||
strokeWidth: '2',
|
strokeWidth: '2',
|
||||||
stroke: '#FFFFFF',
|
stroke: '#FFFFFF',
|
||||||
},
|
},
|
||||||
propsForBackgroundLines: {
|
|
||||||
strokeDasharray: '3,3',
|
|
||||||
stroke: '#E3E6F4',
|
|
||||||
strokeWidth: 1,
|
|
||||||
},
|
|
||||||
fillShadowGradientFromOpacity: 0.1,
|
fillShadowGradientFromOpacity: 0.1,
|
||||||
fillShadowGradientToOpacity: 0.02,
|
fillShadowGradientToOpacity: 0.02,
|
||||||
}}
|
}}
|
||||||
@@ -475,14 +470,88 @@ function trimHeartRateSeries(series: WorkoutDetailMetrics['heartRateSeries']) {
|
|||||||
return series;
|
return series;
|
||||||
}
|
}
|
||||||
|
|
||||||
const step = Math.ceil(series.length / HEART_RATE_CHART_MAX_POINTS);
|
// 智能采样算法:保留重要特征点(峰值、谷值、变化率大的点)
|
||||||
const reduced = series.filter((_, index) => index % step === 0);
|
const result: typeof series = [];
|
||||||
|
const n = series.length;
|
||||||
|
|
||||||
if (reduced[reduced.length - 1] !== series[series.length - 1]) {
|
// 总是保留第一个和最后一个点
|
||||||
reduced.push(series[series.length - 1]);
|
result.push(series[0]);
|
||||||
|
|
||||||
|
// 计算心率变化率
|
||||||
|
const changeRates: number[] = [];
|
||||||
|
for (let i = 1; i < n; i++) {
|
||||||
|
const prevValue = series[i - 1].value;
|
||||||
|
const currValue = series[i].value;
|
||||||
|
const prevTime = dayjs(series[i - 1].timestamp).valueOf();
|
||||||
|
const currTime = dayjs(series[i].timestamp).valueOf();
|
||||||
|
const timeDiff = Math.max(currTime - prevTime, 1000); // 至少1秒,避免除零
|
||||||
|
const valueDiff = Math.abs(currValue - prevValue);
|
||||||
|
changeRates.push(valueDiff / timeDiff * 1000); // 变化率:每秒变化量
|
||||||
}
|
}
|
||||||
|
|
||||||
return reduced;
|
// 计算变化率的阈值(前75%的分位数)
|
||||||
|
const sortedRates = [...changeRates].sort((a, b) => a - b);
|
||||||
|
const thresholdIndex = Math.floor(sortedRates.length * 0.75);
|
||||||
|
const changeThreshold = sortedRates[thresholdIndex] || 0;
|
||||||
|
|
||||||
|
// 识别局部极值点
|
||||||
|
const isLocalExtremum = (index: number): boolean => {
|
||||||
|
if (index === 0 || index === n - 1) return false;
|
||||||
|
|
||||||
|
const prev = series[index - 1].value;
|
||||||
|
const curr = series[index].value;
|
||||||
|
const next = series[index + 1].value;
|
||||||
|
|
||||||
|
// 局部最大值
|
||||||
|
if (curr > prev && curr > next) return true;
|
||||||
|
// 局部最小值
|
||||||
|
if (curr < prev && curr < next) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 遍历所有点,选择重要点
|
||||||
|
let minDistance = Math.max(1, Math.floor(n / HEART_RATE_CHART_MAX_POINTS));
|
||||||
|
|
||||||
|
for (let i = 1; i < n - 1; i++) {
|
||||||
|
const shouldKeep =
|
||||||
|
// 1. 是局部极值点
|
||||||
|
isLocalExtremum(i) ||
|
||||||
|
// 2. 变化率超过阈值
|
||||||
|
(i > 0 && changeRates[i - 1] > changeThreshold) ||
|
||||||
|
// 3. 均匀分布的点(确保基本覆盖)
|
||||||
|
(i % minDistance === 0);
|
||||||
|
|
||||||
|
if (shouldKeep) {
|
||||||
|
// 检查与上一个选中点的距离,避免过于密集
|
||||||
|
const lastSelectedIndex = result.length > 0 ?
|
||||||
|
series.findIndex(p => p.timestamp === result[result.length - 1].timestamp) : 0;
|
||||||
|
|
||||||
|
if (i - lastSelectedIndex >= minDistance || isLocalExtremum(i)) {
|
||||||
|
result.push(series[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保最后一个点被包含
|
||||||
|
if (result[result.length - 1].timestamp !== series[n - 1].timestamp) {
|
||||||
|
result.push(series[n - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果结果仍然太多,进行二次采样
|
||||||
|
if (result.length > HEART_RATE_CHART_MAX_POINTS) {
|
||||||
|
const finalStep = Math.ceil(result.length / HEART_RATE_CHART_MAX_POINTS);
|
||||||
|
const finalResult = result.filter((_, index) => index % finalStep === 0);
|
||||||
|
|
||||||
|
// 确保最后一个点被包含
|
||||||
|
if (finalResult[finalResult.length - 1] !== result[result.length - 1]) {
|
||||||
|
finalResult.push(result[result.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHeartRateZone(zone: HeartRateZoneStat) {
|
function renderHeartRateZone(zone: HeartRateZoneStat) {
|
||||||
@@ -745,6 +814,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
chartStyle: {
|
chartStyle: {
|
||||||
marginLeft: -10,
|
marginLeft: -10,
|
||||||
|
marginRight: -10,
|
||||||
},
|
},
|
||||||
chartEmpty: {
|
chartEmpty: {
|
||||||
paddingVertical: 32,
|
paddingVertical: 32,
|
||||||
|
|||||||
@@ -785,7 +785,7 @@ export async function fetchOxygenSaturation(options: HealthDataOptions): Promise
|
|||||||
export async function fetchHeartRateSamplesForRange(
|
export async function fetchHeartRateSamplesForRange(
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date,
|
endDate: Date,
|
||||||
limit: number = 2000
|
limit: number = 500
|
||||||
): Promise<HeartRateSample[]> {
|
): Promise<HeartRateSample[]> {
|
||||||
try {
|
try {
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
@@ -1138,305 +1138,6 @@ export class StandReminderHelpers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 每日总结通知助手
|
|
||||||
*/
|
|
||||||
export class DailySummaryNotificationHelpers {
|
|
||||||
/**
|
|
||||||
* 获取当日数据汇总
|
|
||||||
*/
|
|
||||||
static async getDailySummaryData(date: string = new Date().toISOString().split('T')[0]) {
|
|
||||||
try {
|
|
||||||
console.log('获取每日汇总数据:', date);
|
|
||||||
|
|
||||||
// 动态导入相关服务,避免循环依赖
|
|
||||||
const { getDietRecords } = await import('@/services/dietRecords');
|
|
||||||
const { getDailyMoodCheckins } = await import('@/services/moodCheckins');
|
|
||||||
const { getWaterRecords } = await import('@/services/waterRecords');
|
|
||||||
const { workoutsApi } = await import('@/services/workoutsApi');
|
|
||||||
|
|
||||||
// 设置日期范围
|
|
||||||
const startDate = new Date(`${date}T00:00:00.000Z`).toISOString();
|
|
||||||
const endDate = new Date(`${date}T23:59:59.999Z`).toISOString();
|
|
||||||
|
|
||||||
// 并行获取各项数据
|
|
||||||
const [dietData, moodData, waterData, workoutData] = await Promise.allSettled([
|
|
||||||
getDietRecords({ startDate, endDate, limit: 100 }),
|
|
||||||
getDailyMoodCheckins(date),
|
|
||||||
getWaterRecords({ date, limit: 100 }),
|
|
||||||
workoutsApi.getTodayWorkout()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 处理饮食数据
|
|
||||||
const dietSummary = {
|
|
||||||
hasRecords: false,
|
|
||||||
mealCount: 0,
|
|
||||||
recordCount: 0
|
|
||||||
};
|
|
||||||
if (dietData.status === 'fulfilled' && dietData.value.records.length > 0) {
|
|
||||||
dietSummary.hasRecords = true;
|
|
||||||
dietSummary.recordCount = dietData.value.records.length;
|
|
||||||
dietSummary.mealCount = new Set(dietData.value.records.map(r => r.mealType)).size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理心情数据
|
|
||||||
const moodSummary = {
|
|
||||||
hasRecords: false,
|
|
||||||
recordCount: 0,
|
|
||||||
latestMood: null as string | null
|
|
||||||
};
|
|
||||||
if (moodData.status === 'fulfilled' && moodData.value.length > 0) {
|
|
||||||
moodSummary.hasRecords = true;
|
|
||||||
moodSummary.recordCount = moodData.value.length;
|
|
||||||
moodSummary.latestMood = moodData.value[0].moodType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理饮水数据
|
|
||||||
const waterSummary = {
|
|
||||||
hasRecords: false,
|
|
||||||
recordCount: 0,
|
|
||||||
totalAmount: 0,
|
|
||||||
completionRate: 0
|
|
||||||
};
|
|
||||||
if (waterData.status === 'fulfilled' && waterData.value.records.length > 0) {
|
|
||||||
waterSummary.hasRecords = true;
|
|
||||||
waterSummary.recordCount = waterData.value.records.length;
|
|
||||||
waterSummary.totalAmount = waterData.value.records.reduce((sum, r) => sum + r.amount, 0);
|
|
||||||
// 假设默认目标是2000ml,实际应该从用户设置获取
|
|
||||||
const dailyGoal = 2000;
|
|
||||||
waterSummary.completionRate = Math.round((waterSummary.totalAmount / dailyGoal) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理锻炼数据
|
|
||||||
const workoutSummary = {
|
|
||||||
hasWorkout: false,
|
|
||||||
isCompleted: false,
|
|
||||||
exerciseCount: 0,
|
|
||||||
completedCount: 0,
|
|
||||||
duration: 0
|
|
||||||
};
|
|
||||||
if (workoutData.status === 'fulfilled' && workoutData.value) {
|
|
||||||
workoutSummary.hasWorkout = true;
|
|
||||||
workoutSummary.isCompleted = workoutData.value.status === 'completed';
|
|
||||||
workoutSummary.exerciseCount = workoutData.value.exercises?.length || 0;
|
|
||||||
workoutSummary.completedCount = workoutData.value.exercises?.filter(e => e.status === 'completed').length || 0;
|
|
||||||
if (workoutData.value.completedAt && workoutData.value.startedAt) {
|
|
||||||
workoutSummary.duration = Math.round((new Date(workoutData.value.completedAt).getTime() - new Date(workoutData.value.startedAt).getTime()) / (1000 * 60));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
date,
|
|
||||||
diet: dietSummary,
|
|
||||||
mood: moodSummary,
|
|
||||||
water: waterSummary,
|
|
||||||
workout: workoutSummary
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取每日汇总数据失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成每日总结推送消息
|
|
||||||
*/
|
|
||||||
static generateDailySummaryMessage(userName: string, summaryData: any): { title: string; body: string } {
|
|
||||||
const { diet, mood, water, workout } = summaryData;
|
|
||||||
|
|
||||||
// 计算完成的项目数量
|
|
||||||
const completedItems = [];
|
|
||||||
const encouragementItems = [];
|
|
||||||
|
|
||||||
// 饮食记录检查
|
|
||||||
if (diet.hasRecords) {
|
|
||||||
completedItems.push(`记录了${diet.mealCount}餐饮食`);
|
|
||||||
} else {
|
|
||||||
encouragementItems.push('饮食记录');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 心情记录检查
|
|
||||||
if (mood.hasRecords) {
|
|
||||||
completedItems.push(`记录了心情状态`);
|
|
||||||
} else {
|
|
||||||
encouragementItems.push('心情记录');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 饮水记录检查
|
|
||||||
if (water.hasRecords) {
|
|
||||||
if (water.completionRate >= 80) {
|
|
||||||
completedItems.push(`完成了${water.completionRate}%的饮水目标`);
|
|
||||||
} else {
|
|
||||||
completedItems.push(`喝水${water.completionRate}%`);
|
|
||||||
encouragementItems.push('多喝水');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
encouragementItems.push('饮水记录');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 锻炼记录检查
|
|
||||||
if (workout.hasWorkout) {
|
|
||||||
if (workout.isCompleted) {
|
|
||||||
completedItems.push(`完成了${workout.duration}分钟锻炼`);
|
|
||||||
} else {
|
|
||||||
completedItems.push(`开始了锻炼训练`);
|
|
||||||
encouragementItems.push('完成锻炼');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
encouragementItems.push('运动锻炼');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成标题和内容
|
|
||||||
let title = '今日健康总结';
|
|
||||||
let body = '';
|
|
||||||
|
|
||||||
if (completedItems.length > 0) {
|
|
||||||
if (completedItems.length >= 3) {
|
|
||||||
// 完成度很高的鼓励
|
|
||||||
const titles = ['今天表现棒极了!', '健康习惯养成中!', '今日收获满满!'];
|
|
||||||
title = titles[Math.floor(Math.random() * titles.length)];
|
|
||||||
body = `${userName},今天您${completedItems.join('、')},真的很棒!`;
|
|
||||||
|
|
||||||
if (encouragementItems.length > 0) {
|
|
||||||
body += `明天在${encouragementItems.join('、')}方面再加把劲哦~`;
|
|
||||||
} else {
|
|
||||||
body += '继续保持这样的好习惯!🌟';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 中等完成度的鼓励
|
|
||||||
title = '今日健康小结';
|
|
||||||
body = `${userName},今天您${completedItems.join('、')},已经很不错了!`;
|
|
||||||
|
|
||||||
if (encouragementItems.length > 0) {
|
|
||||||
body += `明天记得关注一下${encouragementItems.join('、')},让健康生活更完整~`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 完成度较低的温柔提醒
|
|
||||||
const titles = ['明天是新的开始', '健康从每一天开始', '小步前进也是进步'];
|
|
||||||
title = titles[Math.floor(Math.random() * titles.length)];
|
|
||||||
body = `${userName},今天可能比较忙碌。明天记得关注${encouragementItems.slice(0, 2).join('和')},每一个小改变都是向健康生活迈进的一步~💪`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { title, body };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送每日总结推送
|
|
||||||
*/
|
|
||||||
static async sendDailySummaryNotification(userName: string, date?: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
console.log('开始发送每日总结推送...');
|
|
||||||
|
|
||||||
// 检查是否启用了通知
|
|
||||||
if (!(await getNotificationEnabled())) {
|
|
||||||
console.log('用户未启用通知功能,跳过每日总结推送');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取当日数据汇总
|
|
||||||
const summaryData = await this.getDailySummaryData(date);
|
|
||||||
console.log('每日汇总数据:', summaryData);
|
|
||||||
|
|
||||||
// 生成推送消息
|
|
||||||
const { title, body } = this.generateDailySummaryMessage(userName, summaryData);
|
|
||||||
|
|
||||||
// 发送通知
|
|
||||||
await notificationService.sendImmediateNotification({
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
data: {
|
|
||||||
type: 'daily_summary',
|
|
||||||
date: summaryData.date,
|
|
||||||
summaryData,
|
|
||||||
url: '/statistics' // 跳转到统计页面
|
|
||||||
},
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('每日总结推送发送成功');
|
|
||||||
return true;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('发送每日总结推送失败:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安排每日总结推送(每天晚上9点)
|
|
||||||
*/
|
|
||||||
static async scheduleDailySummaryNotification(
|
|
||||||
userName: string,
|
|
||||||
hour: number = 21,
|
|
||||||
minute: number = 0
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
// 检查是否已经存在每日总结提醒
|
|
||||||
const existingNotifications = await notificationService.getAllScheduledNotifications();
|
|
||||||
|
|
||||||
const existingSummaryReminder = existingNotifications.find(
|
|
||||||
notification =>
|
|
||||||
notification.content.data?.type === 'daily_summary_reminder' &&
|
|
||||||
notification.content.data?.isDailyReminder === true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingSummaryReminder) {
|
|
||||||
console.log('每日总结推送已存在,跳过重复注册:', existingSummaryReminder.identifier);
|
|
||||||
return existingSummaryReminder.identifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建每日总结推送通知
|
|
||||||
const notificationId = await notificationService.scheduleCalendarRepeatingNotification(
|
|
||||||
{
|
|
||||||
title: '今日健康总结',
|
|
||||||
body: `${userName},来看看今天的健康生活总结吧~每一份记录都是成长的足迹!✨`,
|
|
||||||
data: {
|
|
||||||
type: 'daily_summary_reminder',
|
|
||||||
isDailyReminder: true,
|
|
||||||
url: '/statistics'
|
|
||||||
},
|
|
||||||
sound: true,
|
|
||||||
priority: 'normal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: Notifications.SchedulableTriggerInputTypes.DAILY,
|
|
||||||
hour: hour,
|
|
||||||
minute: minute,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('每日总结推送已安排,ID:', notificationId);
|
|
||||||
return notificationId;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('安排每日总结推送失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消每日总结推送
|
|
||||||
*/
|
|
||||||
static async cancelDailySummaryNotification(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const notifications = await notificationService.getAllScheduledNotifications();
|
|
||||||
|
|
||||||
for (const notification of notifications) {
|
|
||||||
if (notification.content.data?.type === 'daily_summary_reminder' &&
|
|
||||||
notification.content.data?.isDailyReminder === true) {
|
|
||||||
await notificationService.cancelNotification(notification.identifier);
|
|
||||||
console.log('已取消每日总结推送:', notification.identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('取消每日总结推送失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知模板
|
* 通知模板
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user