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

服务端保留变量与临时文件时,如何实现客户端与服务端多请求交互?

解决方案:中断式文件处理+上下文持久化

核心逻辑

HTTP是无状态协议,单次请求结束后服务端内存变量会被销毁,所以必须把处理过程拆分为多阶段请求,同时在服务端持久化每个阶段的上下文(临时文件、中间变量、处理状态)。客户端收到决策请求后,主动携带上下文标识发起新请求,服务端通过标识恢复上下文继续处理。


PHP 实现方案

1. 第一阶段:上传文件+触发决策请求

处理上传并生成唯一process_id,把临时文件、中间变量存入会话(Session),返回决策提示和process_id:

session_start();
// 生成唯一处理ID,避免冲突
$process_id = uniqid('file_proc_', true);

// 接收上传数据(示例为POST参数,实际可改为$_FILES处理文件)
$fileData = $_POST['initRequest'];
// 保存临时文件到服务器临时目录
$temp_file = sys_get_temp_dir() . "/{$process_id}_temp.dat";
file_put_contents($temp_file, $fileData);

// 上下文存入Session
$_SESSION[$process_id] = [
    'temp_file' => $temp_file,
    'stage' => 'await_decision',
    // 可添加其他需要保留的中间变量
];

// 返回决策请求
echo json_encode([
    'code' => 200,
    'msg' => 'Make Decision',
    'process_id' => $process_id
]);

2. 客户端处理决策并发起续处理请求

拿到process_id后展示决策UI,用户选择后携带ID发送请求:

fetch('/api/init', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({initRequest: 'your_file_data'})
})
.then(res => res.json())
.then(res => {
    if(res.code === 200 && res.msg === 'Make Decision'){
        // 渲染决策按钮
        const decisionPanel = document.createElement('div');
        decisionPanel.innerHTML = `
            <button onclick="submitDecision('${res.process_id}', 'option1')">选项1</button>
            <button onclick="submitDecision('${res.process_id}', 'option2')">选项2</button>
        `;
        document.body.appendChild(decisionPanel);
    }
});

function submitDecision(processId, decision){
    fetch('/api/continue', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({process_id: processId, decision: decision})
    })
    .then(res => res.json())
    .then(res => {
        if(res.code === 200){
            // 触发文件下载
            window.location.href = res.download_url;
            // 清理资源
            fetch('/api/cleanup', {
                method: 'POST',
                body: JSON.stringify({process_id: processId})
            });
        }
    });
}

3. 第二阶段:恢复上下文继续处理

通过process_id从Session读取上下文,根据用户决策完成处理:

session_start();
$input = json_decode(file_get_contents('php://input'), true);
$process_id = $input['process_id'];
$decision = $input['decision'];

// 校验上下文是否存在
if(!isset($_SESSION[$process_id])){
    echo json_encode(['code' => 400, 'msg' => '无效的处理ID']);
    exit;
}

$context = $_SESSION[$process_id];
$temp_file = $context['temp_file'];
$fileData = file_get_contents($temp_file);

// 根据决策执行后续逻辑
if($decision === 'option1'){
    $output_file = sys_get_temp_dir() . "/{$process_id}_output1.pdf";
    file_put_contents($output_file, "按选项1处理:{$fileData}");
} else {
    $output_file = sys_get_temp_dir() . "/{$process_id}_output2.pdf";
    file_put_contents($output_file, "按选项2处理:{$fileData}");
}

// 返回下载链接
echo json_encode([
    'code' => 200,
    'download_url' => "/api/download?file=" . basename($output_file)
]);

4. 资源清理接口

避免临时文件和会话占用资源:

session_start();
$input = json_decode(file_get_contents('php://input'), true);
$process_id = $input['process_id'];

if(isset($_SESSION[$process_id])){
    $context = $_SESSION[$process_id];
    // 删除临时文件
    if(file_exists($context['temp_file'])) unlink($context['temp_file']);
    // 删除输出文件
    $output_file = sys_get_temp_dir() . "/" . basename($_GET['file'] ?? '');
    if(file_exists($output_file)) unlink($output_file);
    // 清除会话上下文
    unset($_SESSION[$process_id]);
}

echo json_encode(['code' => 200, 'msg' => '清理完成']);

Node.js (Express) 实现方案

1. 依赖安装

npm install express express-session multer

2. 服务端核心代码

const express = require('express');
const session = require('express-session');
const multer = require('multer');
const fs = require('fs');
const path = require('path');

const app = express();
const upload = multer({ dest: path.join(__dirname, 'temp') });

// 配置会话存储
app.use(session({
    secret: 'your_secret_key',
    resave: false,
    saveUninitialized: false,
    cookie: { maxAge: 3600000 } // 1小时过期
}));
app.use(express.json());

// 第一阶段:上传文件并触发决策请求
app.post('/api/init', upload.single('file'), (req, res) => {
    const processId = `file_proc_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
    const tempFile = req.file.path;

    // 保存上下文到会话
    req.session[processId] = {
        tempFile,
        originalName: req.file.originalname,
        stage: 'await_decision'
    };

    res.json({
        code: 200,
        msg: 'Make Decision',
        process_id: processId
    });
});

// 第二阶段:接收决策并继续处理
app.post('/api/continue', (req, res) => {
    const { process_id, decision } = req.body;
    const context = req.session[process_id];

    if(!context) return res.status(400).json({ code: 400, msg: '无效的处理ID' });

    fs.readFile(context.tempFile, (err, fileData) => {
        if(err) return res.status(500).json({ code: 500, msg: '读取临时文件失败' });

        // 根据决策生成输出文件
        const outputDir = path.join(__dirname, 'output');
        if(!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);
        
        const outputFile = path.join(outputDir, `${processId}_${decision}_${context.originalName}`);
        fs.writeFileSync(outputFile, `按${decision}处理:${fileData.toString()}`);

        res.json({
            code: 200,
            download_url: `/api/download?file=${path.basename(outputFile)}`
        });
    });
});

// 文件下载接口
app.get('/api/download', (req, res) => {
    const fileName = req.query.file;
    const filePath = path.join(__dirname, 'output', fileName);
    res.download(filePath, err => {
        if(err) res.status(404).json({ code: 404, msg: '文件不存在' });
    });
});

// 资源清理接口
app.post('/api/cleanup', (req, res) => {
    const { process_id } = req.body;
    const context = req.session[process_id];

    if(context){
        // 删除临时文件
        if(fs.existsSync(context.tempFile)) fs.unlinkSync(context.tempFile);
        // 删除输出文件
        const outputFiles = fs.readdirSync(path.join(__dirname, 'output'));
        outputFiles.forEach(file => {
            if(file.startsWith(process_id)) fs.unlinkSync(path.join(__dirname, 'output', file));
        });
        // 清除会话上下文
        delete req.session[process_id];
    }

    res.json({ code: 200, msg: '清理完成' });
});

// 创建临时目录和输出目录
['temp', 'output'].forEach(dir => {
    const dirPath = path.join(__dirname, dir);
    if(!fs.existsSync(dirPath)) fs.mkdirSync(dirPath);
});

app.listen(3000, () => console.log('服务运行在端口3000'));

WebSocket 实时方案(可选)

如果需要服务端异步触发决策请求(比如处理过程中动态遇到分支),可以用WebSocket保持长连接,无需多次发起HTTP请求:

服务端(Node.js + ws)

const WebSocket = require('ws');
const fs = require('fs');
const path = require('path');

const wss = new WebSocket.Server({ port: 8080 });
const connections = new Map(); // 存储连接上下文

wss.on('connection', ws => {
    const connId = `conn_${Date.now()}`;
    connections.set(connId, { ws, stage: 'init' });

    ws.on('message', msg => {
        const data = JSON.parse(msg);
        const context = connections.get(connId);

        switch(data.type){
            case 'upload':
                // 保存临时文件
                const tempFile = path.join(__dirname, 'temp', `${connId}_temp.dat`);
                fs.writeFileSync(tempFile, data.fileData);
                context.tempFile = tempFile;
                context.stage = 'processing';

                // 模拟处理到分支点
                setTimeout(() => {
                    ws.send(JSON.stringify({
                        type: 'decision_request',
                        options: ['option1', 'option2']
                    }));
                    context.stage = 'await_decision';
                }, 1000);
                break;

            case 'decision':
                if(context.stage !== 'await_decision'){
                    ws.send(JSON.stringify({ type: 'error', msg: '无效阶段' }));
                    return;
                }

                // 读取临时文件并处理
                const fileData = fs.readFileSync(context.tempFile);
                const outputFile = path.join(__dirname, 'output', `${connId}_${data.decision}.dat`);
                fs.writeFileSync(outputFile, `按${data.decision}处理:${fileData.toString()}`);

                // 返回下载链接
                ws.send(JSON.stringify({
                    type: 'done',
                    download_url: `http://localhost:3000/download?file=${path.basename(outputFile)}`
                }));

                // 清理资源
                fs.unlinkSync(context.tempFile);
                connections.delete(connId);
                break;
        }
    });

    ws.on('close', () => {
        // 清理未处理资源
        const context = connections.get(connId);
        if(context?.tempFile && fs.existsSync(context.tempFile)){
            fs.unlinkSync(context.tempFile);
        }
        connections.delete(connId);
    });
});

客户端代码

const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
    // 上传文件数据(实际场景可读取本地文件)
    ws.send(JSON.stringify({
        type: 'upload',
        fileData: 'test_file_content'
    }));
};

ws.onmessage = event => {
    const data = JSON.parse(event.data);
    switch(data.type){
        case 'decision_request':
            // 渲染决策UI
            const panel = document.createElement('div');
            data.options.forEach(opt => {
                const btn = document.createElement('button');
                btn.textContent = opt;
                btn.onclick = () => {
                    ws.send(JSON.stringify({ type: 'decision', decision: opt }));
                    panel.remove();
                };
                panel.appendChild(btn);
            });
            document.body.appendChild(panel);
            break;
        case 'done':
            window.location.href = data.download_url;
            ws.close();
            break;
    }
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 02:27:08