Unity HttpClient 之 使用MultipartFormDataContent 发起 内容类型为 multipart/form-data 的数据 Post 请求(正常与流式响应处理)

Unity HttpClient 之 使用MultipartFormDataContent 发起 内容类型为 multipart/form-data 的数据 Post 请求(正常与流式响应处理)

目录

Unity HttpClient 之 使用MultipartFormDataContent 发起 内容类型为 multipart/form-data 的数据 Post 请求(正常与流式响应处理)

一、简单介绍

二、实现原理

三、注意事项

四、示例效果

五、示例实现简单步骤

六、关键脚本


一、简单介绍

Unity 在开发中,网络访问:

    可以使用 UnityWebRequest 访问,不过好似只能用协程的方式,并且访问只能在主线程中;
    所以这里使用 C# 中的 HttpClient,进行网络访问,而且 HttpClient,允许在子线程中访问,在一些情况下可以大大减少主线程的网络访问压力;

这里使用 HttpClient,进行 Post 访问,发请Content-Type内容类型为 multipart/form-data 的请求,并且Task 结合 async (await) 的进行异步访问,最后使用正常方式以及 Stream 流式的形式获取数据,在这里做一个简单的记录,以便后期使用的时候参考。

示例请求参数如下:

1)Header参数:

参数名参数值参数类型必填备注

Content-Type

multipart/form-data

String

2)Body参数:

参数名参数类型备注是否必填

id

Integer

应用id

必填

conversationId

String

对话id

必填

deviceId

String

设备号Id

必填

text

String

对话内容

必填

file

File

文件

必填

type

String

对话类型 "text":文本, "image":图片

必填

二、实现原理

1、HttpClient 创建 Http 客户体

2、MultipartFormDataContent 创建 multipart/form-data 数据体,文件使用注意转为二进制数组(ByteArrayContent)

3、使用 HttpClient.PostAsync 发起请求,HttpResponseMessage 接收结果

4、其中 HttpResponseMessage.Content.ReadAsStringAsync().Result 接收正常的数据结果

5、其中 Stream stream = await HttpResponseMessage.Content.ReadAsStreamAsync()流式获取数据,然后 StreamReader reader = new StreamReader(stream) 和

string line = await reader.ReadLineAsync() 流式读取解析每一行数据

三、注意事项

1、根据服务端返回数据的形式,选择合适的读取响应的数据

2、实现形式仅供参考,如有更好的可以留言讨论

四、示例效果

五、示例实现简单步骤

1、创建一个 Unity工程,添加一个空物体用来挂载测试脚本

2、添加脚本,实现 HttpClient 发起 multipart/form-data Post 请求,包括异步发起和流式接收响应数据

3、把测试脚本挂载到场景物体上

4、运行场景,可以看到打印信息

六、关键脚本

1、HttpClientMultipartFormDataContentRequest


using System.IO;
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Net.Http;

/// <summary>
/// 对话响应结果
/// </summary>
public class ResponeseResult { 
    public string ThreadId { get; set; }
    public string ChatAnswer { get; set; }
}
/// <summary>
/// Http Client 发起 Content-Type 内容类型 multipart / form - data 的请求 实例类
/// </summary>
public class HttpClientMultipartFormDataContentRequest : Singleton<HttpClientMultipartFormDataContentRequest>, IHttpClientMultipartFormDataContentRequest
{
    #region Data
    /// <summary>
    /// TAG
    /// </summary>
    const string TAG = "[GptChatHttpWebRequest] ";

    /// <summary>
    /// 网址 URL_BASE
    /// </summary>
    const string URL_BASE = "https://YourHttpURL.com";
    /// <summary>
    /// URL_VERSION
    /// </summary>
    const string URL_VERSION = "/xrlauncherhwapi/v1";
    /// <summary>
    /// URL_TARGET
    /// </summary>
    const string URL_TARGET = "/gpts/chat";
    /// <summary>
    /// URL
    /// </summary>
    string URL = URL_BASE + URL_VERSION + URL_TARGET;

    #endregion

    #region SendChatRequest

    /// <summary>
    /// 发起对话请求
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="filepath"></param>
    /// <param name="filename"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    /// <returns></returns>
    public ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
        Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Dictionary<string, object> keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);

        return PostFormData(URL, keyValues, filepath, filename, onSuccess, onFailed);
    }

    /// <summary>
    /// 发起对话请求
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name="filename"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    /// <returns></returns>
    public ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Dictionary<string, object> keyValues = DataToDictionary(id,conversationId,deviceId,chatAsk,type);

        return PostFormData(URL, keyValues, fileBinaryData, filename, onSuccess, onFailed);
    }

    /// <summary>
    /// Post multipart/form-data 数据
    /// </summary>
    /// <param name="url"><服务器地址>
    /// <param name="dic"></param>
    /// <param name="filepath"><文件的本地储存地址:包括文件名>
    /// <param name="filename"><文件的名字>
    /// <returns></returns>
    private ResponeseResult PostFormData(string url, Dictionary<string, object> dic, string filepath, string filename,
        Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        return PostFormData(url, dic, File.ReadAllBytes(filepath), filename, onSuccess, onFailed);
    }

    /// <summary>
    /// Post multipart/form-data 数据
    /// </summary>
    /// <param name="url"></param>
    /// <param name="dic"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name="filename"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    /// <returns></returns>
    private ResponeseResult PostFormData(string url, Dictionary<string, object> dic, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Debug.Log(TAG + " PostFormData fileBinaryData.Length " + fileBinaryData.Length);
        string str = "";
        ResponeseResult chatResponeseResult = null;
        try
        {
            HttpClient client = new HttpClient();
            var postContent = new MultipartFormDataContent();
            string boundary = string.Format("--{0}", DateTime.Now.Ticks.ToString("x"));
            postContent.Headers.Add("ContentType", $"multipart/form-data, boundary={boundary}");
            string filekeyname = "file";
            postContent.Add(new ByteArrayContent(fileBinaryData), filekeyname, filename);
            foreach (var key in dic.Keys)
            {
                postContent.Add(new StringContent(dic[key].ToString()), key);
            }
            Debug.Log(TAG + " PostFormData Url " + url);
            HttpResponseMessage response = client.PostAsync(url, postContent).Result;
            str = response.Content.ReadAsStringAsync().Result;
            Debug.Log(TAG + " PostFormData response  Content " + str);
            chatResponeseResult = HandleResult(str);
            onSuccess?.Invoke(chatResponeseResult);
        }
        catch (Exception ex)
        {
            Debug.LogError(TAG + "PostJsonData:" + ex.ToString());
            onFailed?.Invoke(ex.ToString());
        }
        return chatResponeseResult;
    }

    /// <summary>
    /// 拼凑数据结果
    /// </summary>
    /// <param name="content"></param>
    /// <returns></returns>
    private ResponeseResult HandleResult(string content) {
        // 将字符串按换行符分割成多行
        string[] lines = content.Split("data");

        Debug.Log(TAG + " HandleResult lines.Length " + lines.Length);

        // 存储结果的列表
        List<string> texts = new List<string>();
        // 存储结果的列表
        List<string> threadIds = new List<string>();

        // 遍历每一行,提取包含 "text":"xx" 的行中的文本内容
        foreach (string line in lines)
        {
            if (line.Contains("text"))
            {
                int startIndex = line.IndexOf("text") + 7; // 找到 "text":" 的位置
                int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置

                string text = line.Substring(startIndex, endIndex - startIndex);
                texts.Add(text);
            }
            if (line.Contains("thread_id"))
            {
                int startIndex = line.IndexOf("thread_id") + 12; // 找到 "thread_id":" 的位置
                int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置

                string threadId = line.Substring(startIndex, endIndex - startIndex);
                threadIds.Add(threadId);
            }
        }

        string stringBuilder = "";
        // 输出结果
        foreach (string text in texts)
        {
            stringBuilder += (text);
        }

        Debug.Log(TAG + " result text " + stringBuilder);
        return new ResponeseResult() { ThreadId = threadIds.Count > 1 ? threadIds[0] : "", ChatAnswer = string.IsNullOrEmpty(stringBuilder) ? "The server returns empty data" : stringBuilder };
    }

    #endregion

    #region SendChatRequestStream

    /// <summary>
    /// 流式处理数据
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="filepath"></param>
    /// <param name="filename"></param>
    /// <param name="onResponesingGettingData"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    public void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
        Action<ResponeseResult> onResponesingGettingData, Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Dictionary<string, object> keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);

        PostFormDataStreamAsync(URL, keyValues, File.ReadAllBytes(filepath), filename, onResponesingGettingData, onSuccess, onFailed);
    }

    /// <summary>
    /// 流式处理数据
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name=""></param>
    /// <param name="filename"></param>
    /// <param name="onResponesingGettingData"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    public void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onResponesingGettingData, Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Dictionary<string, object> keyValues = DataToDictionary(id, conversationId, deviceId, chatAsk, type);

        PostFormDataStreamAsync(URL, keyValues, fileBinaryData, filename, onResponesingGettingData, onSuccess, onFailed);
    }

    /// <summary>
    /// 异步流式处理数据
    /// </summary>
    /// <param name="url"></param>
    /// <param name="dic"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name="filename"></param>
    /// <param name="onResponesingGettingData"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    private async void PostFormDataStreamAsync(string url, Dictionary<string, object> dic, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onResponesingGettingData, Action<ResponeseResult> onSuccess, Action<string> onFailed)
    {
        Debug.Log(TAG + " PostFormDataStreamAsync fileBinaryData.Length " + fileBinaryData.Length);

        try
        {
            using (HttpClient client = new HttpClient())
            using (var postContent = new MultipartFormDataContent())
            {
                string boundary = string.Format("--{0}", DateTime.Now.Ticks.ToString("x"));
                postContent.Headers.Add("ContentType", $"multipart/form-data, boundary={boundary}");
                string filekeyname = "file";
                postContent.Add(new ByteArrayContent(fileBinaryData), filekeyname, filename);
                foreach (var key in dic.Keys)
                {
                    postContent.Add(new StringContent(dic[key].ToString()), key);
                    Debug.Log(TAG + $" PostFormDataStreamAsync StringContent {key} : {dic[key].ToString()} ");
                }

                Debug.Log(TAG + " PostFormDataStreamAsync Url " + url);

                // 使用异步方法发送请求
                using (HttpResponseMessage response = await client.PostAsync(url, postContent))
                {
                    // 检查响应是否成功
                    response.EnsureSuccessStatusCode();

                    // 使用异步方法读取响应流
                    using (Stream stream = await response.Content.ReadAsStreamAsync())
                    using (StreamReader reader = new StreamReader(stream))
                    {
                        ResponeseResult rslt = new ResponeseResult();
                        // 读取每一行并实时处理
                        while (!reader.EndOfStream)
                        {
                            string line = await reader.ReadLineAsync();
                            // 处理当前行的数据
                            ResponeseResult tmp = ProcessResponseLine(line, onResponesingGettingData);
                            if (tmp != null) { rslt.ThreadId = tmp.ThreadId; rslt.ChatAnswer += tmp.ChatAnswer; }
                        }

                        // 处理响应
                        //chatResponeseResult = HandleResult(str);
                        onSuccess?.Invoke(rslt);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Debug.LogError(TAG + "PostFormDataStreamAsync:" + ex.ToString());
            onFailed?.Invoke(ex.ToString());
        }
    }
    /// <summary>
    /// 处理单条数据 (根据自己的数据形式处理)
    /// </summary>
    /// <param name="line"></param>
    /// <param name="onChatResponesingGettingResult"></param>
    private ResponeseResult ProcessResponseLine(string line, Action<ResponeseResult> onChatResponesingGettingResult)
    {
        // 在这里处理每一行的数据
        Debug.Log(TAG + " ProcessResponseLine: Response Line: " + line);
        ResponeseResult rslt = null;

        if (line.Contains("event:close")) { }
        // 可以根据实际需求进行处理
        if (line.Contains("text"))
        {
            int startIndex = line.IndexOf("text") + 7; // 找到 "text":" 的位置
            int endIndex = line.IndexOf("\"", startIndex); // 找到下一个引号位置

            string text = line.Substring(startIndex, endIndex - startIndex);
            string threadId = "";
            if (line.Contains("thread_id"))
            {
                int startIndex_S = line.IndexOf("thread_id") + 12; // 找到 "thread_id":" 的位置
                int endIndex_S = line.IndexOf("\"", startIndex_S); // 找到下一个引号位置

                threadId = line.Substring(startIndex_S, endIndex_S - startIndex_S);
            }

            Debug.Log(TAG + $" ProcessResponseLine: text {text}, threadId {threadId} ");
            rslt = new ResponeseResult() { ThreadId = threadId, ChatAnswer = text };
            onChatResponesingGettingResult.Invoke(rslt);
            return rslt;
        }
        return rslt;
    }

    #endregion

    #region Other
    /// <summary>
    /// 数据转为字典像是
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <returns></returns>
    Dictionary<string, object> DataToDictionary(int id, string conversationId, string deviceId, string chatAsk, string type)
    {
        Dictionary<string, object> keyValues = new Dictionary<string, object>();
        keyValues.Add("id", id);//id 是服务器返回的凭证  这里的凭证只是针对这个项目后端人员定义的
        keyValues.Add("conversationId", conversationId);//conversationId  这里的凭证只是针对这个项目后端人员定义的
        keyValues.Add("deviceId", deviceId);//deviceId  这里的凭证只是针对这个项目后端人员定义的
        keyValues.Add("text", chatAsk);//text  这里的凭证只是针对这个项目后端人员定义的
        keyValues.Add("type", type);//type  这里的凭证只是针对这个项目后端人员定义的

        return keyValues;
    }
    #endregion
}

2、IHttpClientMultipartFormDataContentRequest

using System;

/// <summary>
/// Http Client 发起 Content-Type 内容类型 multipart / form - data 的请求 接口类
/// </summary>
public interface IHttpClientMultipartFormDataContentRequest
{
    /// <summary>
    /// 发起对话请求
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="filepath"></param>
    /// <param name="filename"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    /// <returns></returns>
    ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename, Action<ResponeseResult> onSuccess, Action<string> onFailed);

    /// <summary>
    /// 发起对话请求
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name="filename"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    /// <returns></returns>
    ResponeseResult SendChatRequest(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onSuccess, Action<string> onFailed);

    /// <summary>
    /// 流式处理数据
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="filepath"></param>
    /// <param name="filename"></param>
    /// <param name="onResponesingGettingData"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, string filepath, string filename,
        Action<ResponeseResult> onResponesingGettingData, Action<ResponeseResult> onSuccess, Action<string> onFailed);

    /// <summary>
    /// 流式处理数据
    /// </summary>
    /// <param name="id"></param>
    /// <param name="conversationId"></param>
    /// <param name="deviceId"></param>
    /// <param name="chatAsk"></param>
    /// <param name="type"></param>
    /// <param name="fileBinaryData"></param>
    /// <param name=""></param>
    /// <param name="filename"></param>
    /// <param name="onResponesingGettingData"></param>
    /// <param name="onSuccess"></param>
    /// <param name="onFailed"></param>
    void SendChatRequestStream(int id, string conversationId, string deviceId, string chatAsk, string type, byte[] fileBinaryData, string filename,
        Action<ResponeseResult> onResponesingGettingData, Action<ResponeseResult> onSuccess, Action<string> onFailed);
}

3、TestHttpClientMultipartFormDataContentRequest

using UnityEngine;

public class TestHttpClientMultipartFormDataContentRequest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        //TestNormal();
        TestStream();
    }


    void TestNormal() {
        int id = 1;
        string conversationId = "";
        string deviceId = "abcdef";
        string chatAsk = "你平时喜欢做什么";
        string type = "text";
        string filepath = "E:\\YourPath\\Imgs\\CameraImag.png";
        string filename = "CameraImag.png";
        HttpClientMultipartFormDataContentRequest.Instance.SendChatRequest(
            id, conversationId, deviceId, chatAsk, type, filepath, filename, (rlt) => {
                Debug.Log($" TestHttpClientMultipartFormDataContentRequest ThreadId = {rlt.ThreadId}, ChatAnswer = {rlt.ChatAnswer}");
            }, (error) => {
                Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail error = " + error);
            });
    }

    void TestStream() {
        int id = 1;
        string conversationId = "";
        string deviceId = "abcdef";
        string chatAsk = "你平时喜欢做什么";
        string type = "text";
        string filepath = "E:\\YourPath\\Imgs\\CameraImag.png";
        string filename = "CameraImag.png";
        HttpClientMultipartFormDataContentRequest.Instance.SendChatRequestStream(
            id, conversationId, deviceId, chatAsk, type, filepath, filename, (rlt) => {
                Debug.Log($" TestHttpClientMultipartFormDataContentRequest ThreadId = {rlt.ThreadId}, ChatAnswer = {rlt.ChatAnswer}");
            }, (rslt) => {
                Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail success : ThreadId {rslt.ThreadId}, ChatAnswer {rslt.ChatAnswer}" );
            }, (error) => {
                Debug.Log($" TestHttpClientMultipartFormDataContentRequest Fail error = " + error);
            });
        
    }
}

4、Singleton

using System;

/// <summary>
/// 单例脚本
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class Singleton<T> where T : Singleton<T>
{
    private static T m_Instance;

    private static object m_Locker = new object();

    public static T Instance
    {
        get
        {
            if (m_Instance == null)
            {
                lock (m_Locker)
                {
                    if (m_Instance == null)
                    {
                        m_Instance = Activator.CreateInstance<T>();
                        m_Instance.OnSingletonInit();
                    }
                }
            }

            return m_Instance;
        }
    }

    protected virtual void OnSingletonInit()
    {
    }
}

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Unity中进行multipart/form-data上传,可以使用UnityWebRequest类。具体步骤如下: 1. 创建一个UnityWebRequest对象,并设置请求的URL和请求方法为POST。 2. 设置请求头部信息,包括Content-Type为multipart/form-data,以及其他必要的信息。 3. 构造表单数据,将需要上传的数据按照multipart/form-data格式进行编码,并设置到UnityWebRequest对象中。 4. 发送请求,并等待服务器响应。 以下是一个示例代码: ``` IEnumerator UploadFile(string url, byte[] data) { UnityWebRequest request = new UnityWebRequest(url, "POST"); request.SetRequestHeader("Content-Type", "multipart/form-data; boundary=------------------------boundary"); string boundary = "--------------------------boundary"; byte[] formHeaderBytes = System.Text.Encoding.UTF8.GetBytes("\r\n--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"); byte[] formFooterBytes = System.Text.Encoding.UTF8.GetBytes("\r\n--" + boundary + "--\r\n"); byte[] bodyBytes = new byte[formHeaderBytes.Length + data.Length + formFooterBytes.Length]; System.Buffer.BlockCopy(formHeaderBytes, 0, bodyBytes, 0, formHeaderBytes.Length); System.Buffer.BlockCopy(data, 0, bodyBytes, formHeaderBytes.Length, data.Length); System.Buffer.BlockCopy(formFooterBytes, 0, bodyBytes, formHeaderBytes.Length + data.Length, formFooterBytes.Length); request.uploadHandler = new UploadHandlerRaw(bodyBytes); request.downloadHandler = new DownloadHandlerBuffer(); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { Debug.Log("Upload complete!"); } else { Debug.Log("Error during upload: " + request.error); } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仙魁XAN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值