我有一个后端项目,前端 请求了后端一个接口,这个接口会一直死循环的方式往 HttpServletResponse 中写入音频流, 后端接口每写入一次 (4096个byte)就会flush一次,但前端

我有一个后端项目,前端 请求了后端一个接口,这个接口会一直死循环的方式往 HttpServletResponse 中写入音频流, 后端接口每写入一次 (4096个byte)就会flush一次,但前端调用会一直是pending状态;同样的代码 我放入到我自己的简单Springboot项目中就不会

你提到的这个问题,可能涉及到几个方面的因素,如网络环境、服务器配置、前端代码和后端代码的具体实现等。下面是一些可能导致这个问题的原因和解决办法:

可能的原因

  1. 网络环境和代理设置

    • 网络代理或者负载均衡器可能会干扰持续的流式传输。
    • 检查是否有任何中间代理或者防火墙对长连接有超时设置。
  2. 服务器配置

    • 某些服务器或者应用容器(如Tomcat、Nginx)可能会对长连接有默认的超时设置。
    • 确保服务器配置允许长时间的保持连接,并且配置了足够的线程和资源来处理持续的流式传输。
  3. 前端代码实现

    • 确保前端在接收到部分数据后能够正确处理和展示,而不是等待所有数据传输完毕。
    • 使用 ReadableStream 或者其他支持流式传输的 API 来处理持续的流数据。
  4. 后端代码实现

    • 确保后端在每次写入并 flush 数据后,没有因为某些原因导致阻塞。
    • 检查代码中是否存在阻塞操作,比如同步锁,或者外部资源调用(如数据库查询)导致阻塞。

具体调试和解决方法

  1. 确认前端代码

    fetch('/your-endpoint')
        .then(response => {
            const reader = response.body.getReader();
            return new ReadableStream({
                start(controller) {
                    function push() {
                        reader.read().then(({ done, value }) => {
                            if (done) {
                                controller.close();
                                return;
                            }
                            controller.enqueue(value);
                            push();
                        });
                    }
                    push();
                }
            })
        })
        .then(stream => new Response(stream))
        .then(response => response.blob())
        .then(blob => {
            // handle the audio blob
        });
    

  2. 确认后端代码(假设使用Spring Boot):

    @RestController
    public class AudioStreamController {
    
        @GetMapping("/audio-stream")
        public void streamAudio(HttpServletResponse response) {
            response.setContentType("audio/mpeg");
            response.setHeader("Connection", "keep-alive");
    
            try (OutputStream outputStream = response.getOutputStream()) {
                byte[] buffer = new byte[4096];
                while (true) {
                    // 假设 getAudioData() 获取音频数据
                    int bytesRead = getAudioData(buffer);
                    if (bytesRead == -1) {
                        break;
                    }
                    outputStream.write(buffer, 0, bytesRead);
                    outputStream.flush();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private int getAudioData(byte[] buffer) {
            // 实现获取音频数据的逻辑
            return -1;
        }
    }
    

  3. 检查服务器配置

    • 如果使用Tomcat,在 application.properties 中增加以下配置:
       properties 

      复制代码

      server.connection-timeout=5000

    • 如果使用Nginx,检查 proxy_read_timeoutproxy_send_timeout 的配置。

通过上述步骤和检查,应该能更好地定位和解决前端调用一直处于 pending 状态的问题。如果问题依然存在,请进一步提供详细的前后端代码和配置,以便进行更深入的排查。

DOMException: play() failed because the user didn't interact with the document first

这个错误通常发生在浏览器的自动播放限制上。大多数现代浏览器为了防止网站自动播放不需要的音频或视频,要求用户必须首先与页面进行交互,例如点击页面上的某个元素,然后才能播放音频或视频。

要解决这个问题,你可以在用户进行交互之后再开始播放音频。以下是一个简单的示例:

前端代码示例

  1. HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Audio Stream</title>
    </head>
    <body>
        <button id="playButton">Play Audio</button>
        <script src="app.js"></script>
    </body>
    </html>
    

  2. JavaScript (app.js)

    document.getElementById('playButton').addEventListener('click', () => {
        fetch('/audio-stream')
            .then(response => {
                const reader = response.body.getReader();
                return new ReadableStream({
                    start(controller) {
                        function push() {
                            reader.read().then(({ done, value }) => {
                                if (done) {
                                    controller.close();
                                    return;
                                }
                                controller.enqueue(value);
                                push();
                            });
                        }
                        push();
                    }
                });
            })
            .then(stream => new Response(stream))
            .then(response => response.blob())
            .then(blob => {
                const audioUrl = URL.createObjectURL(blob);
                const audio = new Audio(audioUrl);
                audio.play().catch(error => {
                    console.error('Audio play failed:', error);
                });
            })
            .catch(error => {
                console.error('Fetch error:', error);
            });
    });
    

说明

  1. 用户交互

    • 在 HTML 中添加一个按钮,用户需要点击这个按钮才能开始播放音频。
    • 在 JavaScript 中为按钮添加一个点击事件监听器,当用户点击按钮时才会发起请求并播放音频。
  2. 处理音频流

    • 点击按钮后,发起对后端 /audio-stream 接口的请求。
    • 使用 ReadableStream 来处理持续的流式传输。
    • 将流转换为一个 blob 对象,并创建一个音频 URL。
    • 创建一个新的 Audio 对象并调用 play() 方法播放音频。

注意

  • 确保你的后端接口 /audio-stream 可以正常工作并返回音频流。
  • 这种方法确保用户首先与页面进行交互,满足浏览器的自动播放策略要求。

通过这种方式,可以解决 DOMException: play() failed because the user didn't interact with the document first 这个问题,并且能够正确播放流式传输的音频。

后端 有一个 接口,一直往循环 httpServletResponse 中write数据,然后每次 write 1024个byte就会flush,但前端 都是只能等 后端完全写入完毕才开始从流中拿取数据,我该如何做才能让前端实时获取流中的数据呢;

为了让前端能够实时获取后端流中的数据,确保在每次 writeflush 后,前端能够及时读取流中的数据,您可以按照以下步骤进行:

前端代码

  1. HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Audio Stream</title>
    </head>
    <body>
        <button id="playButton">Play Audio</button>
        <script src="app.js"></script>
    </body>
    </html>
    

  2. JavaScript (app.js)

    document.getElementById('playButton').addEventListener('click', () => {
        fetch('/audio-stream')
            .then(response => {
                const reader = response.body.getReader();
                const stream = new ReadableStream({
                    start(controller) {
                        function push() {
                            reader.read().then(({ done, value }) => {
                                if (done) {
                                    controller.close();
                                    return;
                                }
                                controller.enqueue(value);
                                push();
                            });
                        }
                        push();
                    }
                });
                return new Response(stream);
            })
            .then(response => response.blob())
            .then(blob => {
                const audioUrl = URL.createObjectURL(blob);
                const audio = new Audio(audioUrl);
                audio.play().catch(error => {
                    console.error('Audio play failed:', error);
                });
            })
            .catch(error => {
                console.error('Fetch error:', error);
            });
    });
    

后端代码

假设您使用的是 Spring Boot,后端代码如下:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@RestController
public class AudioStreamController {

    @GetMapping("/audio-stream")
    public void streamAudio(HttpServletResponse response) {
        response.setContentType("audio/mpeg");
        response.setHeader("Connection", "keep-alive");

        try (OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            while (true) {
                int bytesRead = getAudioData(buffer); // 假设 getAudioData() 获取音频数据
                if (bytesRead == -1) {
                    break;
                }
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // 模拟延迟,以便展示实时流式传输效果
                Thread.sleep(100); // 100毫秒延迟
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private int getAudioData(byte[] buffer) {
        // 实现获取音频数据的逻辑
        // 返回读取的字节数,如果没有更多数据则返回 -1
        return -1; // 仅为示例,实际应返回读取的字节数
    }
}

关键点

  1. 前端

    • 使用 ReadableStreamresponse.body.getReader() 来处理流数据。
    • 在每次读取一块数据后,立即将其传给 ReadableStream,这样数据会被实时处理。
  2. 后端

    • 每次 writeflush 后,前端会接收到一部分数据。
    • 使用 Thread.sleep() 模拟延迟,以便展示流式传输效果。

注意事项

  • 确保后端的 getAudioData 方法能正确返回音频数据。
  • 根据实际情况调整缓冲区大小和延迟时间。
  • 在生产环境中,请确保对线程和资源进行正确管理,以防止资源泄露和性能问题。

通过这种方式,前端可以实时接收和处理后端流式传输的数据。

DOMException: play() failed because the user didn't interact with the document first

这个问题是由于现代浏览器的自动播放限制,要求用户在页面上进行交互(例如点击按钮)后才允许播放音频或视频。为了实现实时接收和播放流式传输的音频数据,你可以在用户交互之后再开始流式传输和播放音频。

下面是完整的示例代码,确保前端代码在用户点击按钮后启动流式传输,并处理音频播放:

前端代码

  1. HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Audio Stream</title>
    </head>
    <body>
        <button id="playButton">Play Audio</button>
        <script src="app.js"></script>
    </body>
    </html>
    

  2. JavaScript (app.js)

    document.getElementById('playButton').addEventListener('click', () => {
        fetch('/audio-stream')
            .then(response => {
                const reader = response.body.getReader();
                const audioContext = new (window.AudioContext || window.webkitAudioContext)();
                const source = audioContext.createBufferSource();
                const audioQueue = [];
    
                source.connect(audioContext.destination);
                source.start();
    
                const processAudioQueue = () => {
                    if (audioQueue.length > 0) {
                        const audioData = audioQueue.shift();
                        audioContext.decodeAudioData(audioData.buffer.slice(0), buffer => {
                            source.buffer = buffer;
                            source.start();
                        });
                    }
                    requestAnimationFrame(processAudioQueue);
                };
    
                processAudioQueue();
    
                const readData = () => {
                    reader.read().then(({ done, value }) => {
                        if (done) {
                            return;
                        }
                        audioQueue.push(value);
                        readData();
                    }).catch(error => {
                        console.error('Stream read error:', error);
                    });
                };
    
                readData();
            })
            .catch(error => {
                console.error('Fetch error:', error);
            });
    });
    

后端代码

假设你使用的是 Spring Boot,后端代码如下:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@RestController
public class AudioStreamController {

    @GetMapping("/audio-stream")
    public void streamAudio(HttpServletResponse response) {
        response.setContentType("audio/mpeg");
        response.setHeader("Connection", "keep-alive");

        try (OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            while (true) {
                int bytesRead = getAudioData(buffer); // 假设 getAudioData() 获取音频数据
                if (bytesRead == -1) {
                    break;
                }
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // 模拟延迟,以便展示实时流式传输效果
                Thread.sleep(100); // 100毫秒延迟
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private int getAudioData(byte[] buffer) {
        // 实现获取音频数据的逻辑
        // 返回读取的字节数,如果没有更多数据则返回 -1
        return -1; // 仅为示例,实际应返回读取的字节数
    }
}

关键点

  1. 前端

    • 在用户点击按钮后,开始流式传输和播放音频。
    • 使用 AudioContextBufferSource 实时解码和播放音频数据。
    • 使用 requestAnimationFrame 确保持续处理音频队列。
  2. 后端

    • 每次 writeflush 后,前端会接收到一部分数据。
    • 使用 Thread.sleep() 模拟延迟,以便展示流式传输效果。

注意事项

  • 确保后端的 getAudioData 方法能正确返回音频数据。
  • 根据实际情况调整缓冲区大小和延迟时间。
  • 在生产环境中,请确保对线程和资源进行正确管理,以防止资源泄露和性能问题。

通过这种方式,前端可以在用户交互之后,实时接收和播放后端流式传输的音频数据。

audio muted

为了实现实时音频流式传输并解决自动播放限制和音频静音的问题,我们可以在用户点击按钮后启动音频流,并确保音频不会被静音。以下是详细的实现步骤:

前端代码

  1. HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Audio Stream</title>
    </head>
    <body>
        <button id="playButton">Play Audio</button>
        <script src="app.js"></script>
    </body>
    </html>
    

  2. JavaScript (app.js)

    document.getElementById('playButton').addEventListener('click', () => {
        fetch('/audio-stream')
            .then(response => {
                const reader = response.body.getReader();
                const audioContext = new (window.AudioContext || window.webkitAudioContext)();
                const source = audioContext.createBufferSource();
                const audioQueue = [];
    
                const scriptNode = audioContext.createScriptProcessor(4096, 1, 1);
                scriptNode.onaudioprocess = (audioProcessingEvent) => {
                    if (audioQueue.length > 0) {
                        const audioData = audioQueue.shift();
                        audioContext.decodeAudioData(audioData.buffer.slice(0), buffer => {
                            if (buffer) {
                                const outputBuffer = audioProcessingEvent.outputBuffer;
                                for (let channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
                                    const outputData = outputBuffer.getChannelData(channel);
                                    const inputData = buffer.getChannelData(channel);
                                    for (let sample = 0; sample < inputData.length; sample++) {
                                        outputData[sample] = inputData[sample];
                                    }
                                }
                            }
                        });
                    }
                };
    
                scriptNode.connect(audioContext.destination);
    
                const readData = () => {
                    reader.read().then(({ done, value }) => {
                        if (done) {
                            return;
                        }
                        audioQueue.push(value);
                        readData();
                    }).catch(error => {
                        console.error('Stream read error:', error);
                    });
                };
    
                readData();
            })
            .catch(error => {
                console.error('Fetch error:', error);
            });
    });
    

后端代码

假设你使用的是 Spring Boot,后端代码如下:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@RestController
public class AudioStreamController {

    @GetMapping("/audio-stream")
    public void streamAudio(HttpServletResponse response) {
        response.setContentType("audio/mpeg");
        response.setHeader("Connection", "keep-alive");

        try (OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            while (true) {
                int bytesRead = getAudioData(buffer); // 假设 getAudioData() 获取音频数据
                if (bytesRead == -1) {
                    break;
                }
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // 模拟延迟,以便展示实时流式传输效果
                Thread.sleep(100); // 100毫秒延迟
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private int getAudioData(byte[] buffer) {
        // 实现获取音频数据的逻辑
        // 返回读取的字节数,如果没有更多数据则返回 -1
        return -1; // 仅为示例,实际应返回读取的字节数
    }
}

关键点

  1. 前端

    • 在用户点击按钮后,开始流式传输和播放音频。
    • 使用 AudioContextScriptProcessorNode 实时解码和播放音频数据。
    • scriptNode 处理音频数据并将其传递给音频上下文的目标(扬声器)。
  2. 后端

    • 每次 writeflush 后,前端会接收到一部分数据。
    • 使用 Thread.sleep() 模拟延迟,以便展示流式传输效果。

注意事项

  • 确保后端的 getAudioData 方法能正确返回音频数据。
  • 根据实际情况调整缓冲区大小和延迟时间。
  • 在生产环境中,请确保对线程和资源进行正确管理,以防止资源泄露和性能问题。

通过这种方式,前端可以在用户交互之后,实时接收和播放后端流式传输的音频数据,并且不会因为自动播放限制而被静音。

muted干啥的

抱歉,之前的解释可能有些误导。muted 通常是用于静音视频或音频元素的属性。您在前端代码中并没有明确提到muted属性,但如果音频元素被设置为muted,它将会静音。

为确保前端能够实时接收并播放音频流而不会被静音,您可以检查和确保音频元素没有被设置为静音。

这里是一个改进后的前端代码示例,其中直接使用Audio元素来播放音频流,这样可以更好地控制音频播放,并确保音频不会被静音:

前端代码

  1. HTML

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Audio Stream</title>
    </head>
    <body>
        <button id="playButton">Play Audio</button>
        <audio id="audioPlayer" controls></audio>
        <script src="app.js"></script>
    </body>
    </html>
    

  2. JavaScript (app.js)

    document.getElementById('playButton').addEventListener('click', () => {
        const audioElement = document.getElementById('audioPlayer');
        const audioContext = new (window.AudioContext || window.webkitAudioContext)();
        const source = audioContext.createMediaElementSource(audioElement);
        source.connect(audioContext.destination);
    
        fetch('/audio-stream')
            .then(response => {
                const reader = response.body.getReader();
                const stream = new ReadableStream({
                    start(controller) {
                        function push() {
                            reader.read().then(({ done, value }) => {
                                if (done) {
                                    controller.close();
                                    return;
                                }
                                controller.enqueue(value);
                                push();
                            });
                        }
                        push();
                    }
                });
    
                return new Response(stream);
            })
            .then(response => response.blob())
            .then(blob => {
                const audioUrl = URL.createObjectURL(blob);
                audioElement.src = audioUrl;
                audioElement.play().catch(error => {
                    console.error('Audio play failed:', error);
                });
            })
            .catch(error => {
                console.error('Fetch error:', error);
            });
    });
    

后端代码

假设您使用的是 Spring Boot,后端代码如下:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;

@RestController
public class AudioStreamController {

    @GetMapping("/audio-stream")
    public void streamAudio(HttpServletResponse response) {
        response.setContentType("audio/mpeg");
        response.setHeader("Connection", "keep-alive");

        try (OutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            while (true) {
                int bytesRead = getAudioData(buffer); // 假设 getAudioData() 获取音频数据
                if (bytesRead == -1) {
                    break;
                }
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
                // 模拟延迟,以便展示实时流式传输效果
                Thread.sleep(100); // 100毫秒延迟
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    private int getAudioData(byte[] buffer) {
        // 实现获取音频数据的逻辑
        // 返回读取的字节数,如果没有更多数据则返回 -1
        return -1; // 仅为示例,实际应返回读取的字节数
    }
}

关键点

  1. 前端

    • 在用户点击按钮后,开始流式传输和播放音频。
    • 使用Audio元素直接播放音频流,这样更简单且有效。
    • audioElement 确保不会被设置为静音(即没有 muted 属性)。
  2. 后端

    • 每次 writeflush 后,前端会接收到一部分数据。
    • 使用 Thread.sleep() 模拟延迟,以便展示流式传输效果。

注意事项

  • 确保后端的 getAudioData 方法能正确返回音频数据。
  • 根据实际情况调整缓冲区大小和延迟时间。
  • 在生产环境中,请确保对线程和资源进行正确管理,以防止资源泄露和性能问题。

通过这种方式,前端可以在用户交互之后,实时接收和播放后端流式传输的音频数据,并且不会被静音。

后端进行读取的时候,只需要从最后100kb开始读取即可,给我个案例

如果您需要后端在读取音频数据时,只读取文件的最后100KB,并将其流式传输到前端,可以按以下步骤实现。

后端代码

假设您使用的是 Spring Boot,并且音频文件存储在服务器的文件系统中,以下是一个示例代码:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

@RestController
@RequestMapping("/audio")
public class AudioStreamController {

    private static final String FILE_PATH = "/path/to/your/audio/file.mp3";
    private static final int BUFFER_SIZE = 1024;
    private static final int READ_SIZE = 100 * 1024; // 100KB

    @GetMapping("/stream")
    public void streamAudio(HttpServletResponse response) {
        response.setContentType("audio/mpeg");
        response.setHeader("Connection", "keep-alive");

        File audioFile = new File(FILE_PATH);
        long fileSize = audioFile.length();
        long startByte = Math.max(0, fileSize - READ_SIZE);

        try (FileInputStream inputStream = new FileInputStream(audioFile);
             OutputStream outputStream = response.getOutputStream()) {

            inputStream.skip(startByte);
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead;

            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                outputStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关键点

  1. 文件读取

    • 使用 FileInputStream 读取音频文件。
    • inputStream.skip(startByte) 跳过文件开头部分,只读取最后 100KB 的数据。
  2. 流式传输

    • 每次读取 BUFFER_SIZE 大小的数据(1024 字节),并写入到 HttpServletResponse 中。
    • outputStream.flush() 确保数据被及时发送到前端。

前端代码

前端代码将与之前的实现相似,使用 fetch API 来获取音频流并播放:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Audio Stream</title>
</head>
<body>
    <button id="playButton">Play Audio</button>
    <audio id="audioPlayer" controls></audio>
    <script>
        document.getElementById('playButton').addEventListener('click', () => {
            const audioElement = document.getElementById('audioPlayer');

            fetch('/audio/stream')
                .then(response => response.blob())
                .then(blob => {
                    const audioUrl = URL.createObjectURL(blob);
                    audioElement.src = audioUrl;
                    audioElement.play().catch(error => {
                        console.error('Audio play failed:', error);
                    });
                })
                .catch(error => {
                    console.error('Fetch error:', error);
                });
        });
    </script>
</body>
</html>

关键点

  1. 用户交互

    • 在用户点击按钮后,开始流式传输和播放音频。
  2. 音频播放

    • 使用 Audio 元素直接播放从后端获取的音频流。

注意事项

  • 确保文件路径正确,音频文件存在。
  • 根据实际情况调整缓冲区大小和读取大小。
  • 在生产环境中,请确保对资源进行正确管理,以防止资源泄露和性能问题。

通过这种方式,后端只读取音频文件的最后100KB,并将其流式传输到前端进行播放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值