You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Expo推送通知图片在Cron任务中返回Null问题排查

问题说明

我用React Native Expo开发的应用,其他功能正常,但Expo推送通知存在一个问题:通过PHP Cron任务脚本发送通知时,点击通知能正常打开页面,但接收的图片数据为Null;而用Postman手动调用同一个脚本时,图片链接可正常读取,应用中能显示图片,控制台里图片值是有效链接。

我怀疑问题出在这段代码里——根据经验,Cron任务脚本可能无法正确获取document_root来检查文件是否存在:

$image_link_check = $documentRoot . "/admin/uploads/news/" .$latest_news['image'];

if(file_exists($image_link_check)) {
    $image = $imageBaseUrl . "/news/" . $latest_news['image'];
} else {
    $image = NULL;
}
接收页面代码(NewsDetailsScreen.js)
import {
    StyleSheet,
    SafeAreaView,
    Image,
    Button,
    View,
    Platform,
    StatusBar,
    ScrollView,
    TouchableOpacity,
    Text,
} from 'react-native';
import React, { useState, useEffect, useContext } from 'react';
import AppText from '../components/AppText';
import colors from '../config/colors';
import { openBrowserAsync } from 'expo-web-browser';
import { CommonActions, useNavigation } from '@react-navigation/native';

import AsyncStorage from '@react-native-async-storage/async-storage';

import { useFocusEffect } from '@react-navigation/native';

import themeContext from '../theme/themeContext';

export default function NewsDetailsScreen({ route }) {
    const news = route.params;
    const navigation = useNavigation();

    const theme = useContext(themeContext);
    const [author, setAuthor] = useState();
    const [darkModeOn, setDarkModeOn] = useState(false);

    const retrieveDarkMode = async () => {
        try {
            const jsonValue = await AsyncStorage.getItem('darkModeOn');

            if (jsonValue !== null) {
                const parsedValue = JSON.parse(jsonValue);
                setDarkModeOn(parsedValue);
            }
        } catch (e) {
            console.log('error: ', e);
        }
    };

    const displayAuthor = (author) => {
        if (
            author !== 'Editor' &&
            author !== 'F1 Desk' &&
            author !== 'Motorsport Network' &&
            author !== 'RACER Staff' &&
            author !== 'GPblog' &&
            author !== ''
        ) {
            setAuthor('By ' + author);
        } else {
            setAuthor('');
        }
    };

    useEffect(() => {
        displayAuthor(news.author);
        console.log(
            'news: ------------------------------------------------------------------ ',
            news.image
        );
    });

    useFocusEffect(
        React.useCallback(() => {
            retrieveDarkMode();

            return () => {};
        }, [])
    );

    const styles = StyleSheet.create({
        author: {
            color: colors.lightBlue,
            marginTop: 10,
        },
        button: {
            marginTop: 20,
            marginBottom: 20,
        },
        date: {
            marginTop: 10,
            fontSize: 15,
            color: darkModeOn ? colors.white : colors.lightBlack,
        },
        description: {
            marginTop: 10,
            color: darkModeOn ? colors.white : colors.lightBlack,
        },
        detailsContainer: {
            padding: 20,
        },
        image: {
            width: '100%',
            height: 330,
        },
        mainContainer: {
            marginTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
            backgroundColor: theme.backgroundColor,
        },
        source: {
            marginTop: 10,
            color: darkModeOn ? colors.white : colors.lightBlack,
        },
        title: {
            fontSize: 24,
            fontWeight: '500',
            color: darkModeOn ? colors.white : colors.lightBlack,
        },
        backText: {
            color: darkModeOn ? colors.white : colors.lightBlack,
            fontSize: 35,
            paddingLeft: 5,
            paddingBottom: 10,
            position: 'absolute',
            left: 10,
            bottom: '-15%',
        },
    });

    return (
        <ScrollView style={styles.scrollView}>
            <SafeAreaView style={styles.mainContainer}>
                <TouchableOpacity
                    onPress={() => {
                        navigation.dispatch(CommonActions.goBack());
                    }}
                    style={{ width: '100%', height: 45, flexDirection: 'row' }}
                    activeOpacity={1}
                >
                    <Text style={styles.backText}>&#x2039;</Text>
                </TouchableOpacity>
                {news.image !== null ? (
                    <Image
                        style={styles.image}
                        source={{
                            uri: news.image,
                        }}
                    />
                ) : (
                    <Image
                        style={styles.image}
                        source={require('../assets/f1ianIcon300x300.png')}
                    />
                )}

                <SafeAreaView style={styles.detailsContainer}>
                    <AppText style={styles.title}>{news.title}</AppText>
                    <AppText style={styles.description}>
                        {news.description}
                    </AppText>
                    {news.author !== null && (
                        <AppText style={styles.author}>{author}</AppText>
                    )}
                    <AppText style={styles.date}>{news.pubDate}</AppText>
                    <AppText style={styles.source}>
                        From {news.feedName}
                    </AppText>
                    <View style={styles.button}>
                        <Button
                            title="Read Full Article"
                            onPress={() => openBrowserAsync(news.link)}
                        />
                    </View>
                </SafeAreaView>
            </SafeAreaView>
        </ScrollView>
    );
}
PHP脚本相关片段
// 站点链接生成
if(array_key_exists('HTTPS', $_SERVER) && $_SERVER["HTTPS"] == "on") {
    $site_link = "https://".$_SERVER['HTTP_HOST'] . "" . $base_directory;
} else {
    $site_link = "http://".$_SERVER['HTTP_HOST'] . "" . $base_directory;
}

$base_directory = "";
$imageBaseUrl = $site_link . "/admin/uploads";

// 获取最新新闻
$latest_news = find_the_latest_single_news_by_subject_id($f1_subject);
$found_feed = find_feed_info_by_feed_id($latest_news['feed_id']);

// 遍历设备发送通知
while($found_devices_assoc = mysqli_fetch_assoc($found_devices)) {
    $token = $found_devices_assoc['token'];

    // 处理标题
    $title = $latest_news['title'];
    $title = str_replace("&#39;", "'", $title);
    $title = str_replace("&#34;", "'", $title);

    $source = ucwords($found_feed['feed_name']);

    // 处理作者
    $author = str_replace("&#34;", '"', $latest_news['author']);
    $author = str_replace("&#39;", "'", $author);
    $author = str_replace(".com", "", $author);
    $author = trim($author);
    if(strtolower($author) != strtolower($found_feed['feed_name'])) {
        $author = $author;
    } else {
        $author = NULL;
    }

    // 处理描述
    $description = str_replace("&#34;", '"', $latest_news['description']);
    $description = str_replace("&#39;", "'", $description);
    $description = str_replace("\n", " ", $description);

    // 处理日期
    $pubDate = date("F j, Y", $latest_news['pubDate']) . " at " . date("g:i a", $latest_news['pubDate']);
    $insertDateTime = date("F j, Y", $latest_news['insert_date']) . " at " . date("g:i a", $latest_news['insert_date']);

    // 处理图片
    $image = "";
    if($latest_news['image'] === NULL || $latest_news['image'] === "") {
        $image = NULL;
    } else {
        $image_link_check = $documentRoot . "/admin/uploads/news/" . $latest_news['image'];
        echo "image_link_check: " . $image_link_check . "<br><br>";
        if(file_exists($image_link_check)) {
            $image = $imageBaseUrl . "/news/" . $latest_news['image'];
        } else {
            $image = NULL;
        }
    }

    // 发送通知逻辑
    if($found_devices_assoc['notificationOn'] == 1) {
        $json_payload = array(
            'to' => $token,
            'sound' => 'default',
            'title' => $source,
            'body' => $title,
            "data" => [
                "screen" => "NewsDetailsScreen",
                "id" => $latest_news['id'],
                "feedName" => $source,
                "pubDate" => $pubDate,
                "title" => $title,
                "description" => $description,
                "link" => $latest_news['link'],
                "author" => $author,
                "image" => $image,
                "insertDate" => $insertDateTime
            ]
        );

        $data = json_encode($json_payload);
        var_dump($data);
        
        $ch = curl_init();
        curl_setopt_array($ch, array(
            CURLOPT_URL => "https://exp.host/--/api/v2/push/send",
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => "",
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $data,
            CURLOPT_HTTPHEADER => array(
                "Accept: application/json",
                "Accept-Encoding: gzip, deflate",
                "Content-Type: application/json",
                "cache-control: no-cache",
                "host: exp.host"
            ),
        ));

        $response = curl_exec($ch);
        $err = curl_error($ch);
        if ($response === FALSE) {
            die('Oops! FCM Send Error: ' . curl_error($ch));
        }
        curl_close($ch);
        
        if ($err) {
            echo "cURL Error #:" . $err;
        } else {
            echo $response;
        }
    }
}
解决方案

核心问题是Cron环境下$_SERVER变量不全,导致$documentRoot和$site_link获取错误,进而图片检查失败。

1. 修复$documentRoot的获取

Cron运行时没有Web服务器的环境变量,不能依赖$_SERVER['DOCUMENT_ROOT'],直接用绝对路径定义:

// 替换原来的$documentRoot赋值,改成服务器上的真实绝对路径
$documentRoot = '/var/www/your-site-folder'; // 示例路径,替换为你的网站根目录

或者用脚本所在目录推导根目录:

// 假设脚本在网站根目录下的cron文件夹里,用此方式获取根目录
$documentRoot = dirname(__DIR__);

2. 修复$site_link的生成

Cron环境下$_SERVER['HTTP_HOST']不存在,直接硬编码站点域名:

// 替换原来的站点链接生成逻辑
$site_link = "https://your-domain.com"; // 替换为你的实际域名
$base_directory = "";
$imageBaseUrl = $site_link . "/admin/uploads";

3. 可选:跳过文件存在检查(如果数据库路径可靠)

如果数据库里的image字段只会存储真实存在的图片文件名,可以直接生成链接,省去文件存在检查步骤:

if($latest_news['image'] === NULL || $latest_news['image'] === "") {
    $image = NULL;
} else {
    $image = $imageBaseUrl . "/news/" . $latest_news['image'];
}

内容的提问来源于stack exchange,提问作者f1ian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 08:55:55