【人工智能应用技术】-基础实战-小程序应用(基于springAI+百度语音技术)智能语音控制

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 520人参与

智能语音控制系统部分架构图
在这里插入图片描述

我今天主要总结业务中最核心的接入识别部分,第一次做肯定会遇到一些问题总结给大家参考

新手必看|微信小程序与Spring Boot后端交互全流程(踩坑+解决+完整代码)

作为编程新手,第一次做「小程序+后端」联调时,很容易被各种网络问题、字段匹配问题、环境配置问题卡住。本文以「智能设备语音控制小程序」为例,完整记录从0到1实现小程序与Spring Boot后端交互的全过程——包括环境配置、代码编写、遇到的每一个问题及解决方案,全程迭代式讲解,新手也能跟着一步步复刻。
最新效果如图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
真机调试需要关闭防火墙
在这里插入图片描述

一、前置准备:明确需求与环境

1. 核心需求

实现一个简单的语音控制小程序:

  • 小程序端:录音→调用百度语音转文字→将指令(如“打开客厅灯”)传给后端;
  • 后端:解析指令→下发指令到远程设备→后端异步返回指令处理结果→小程序同步设备状态。

2. 必备工具/环境

类型工具/版本新手备注
后端JDK 8+、Spring Boot 2.7+、IDEA确保JDK环境变量配置正确
前端微信开发者工具、小程序账号无需认证,测试阶段用测试号即可
网络同一局域网(电脑+手机)真机测试必须满足此条件
第三方百度语音识别API Key/Secret免费申请,用于语音转文字

二、第一步:后端(Spring Boot)开发(新手友好版)

1. 创建Spring Boot项目

打开IDEA,新建Spring Boot项目,勾选核心依赖:Spring Web(web开发)、Spring AI大模型集成、Lombok(简化代码)。

2. 核心代码编写(迭代1:基础结构)

(1)定义枚举(指令动作/状态)

新手容易忽略枚举的序列化问题,先定义核心枚举:

// 指令动作枚举(打开/关闭)
public enum CommandType {
    OPEN,  // 打开
    CLOSE  // 关闭
}

// 指令状态枚举(执行中/成功/失败)
public enum CommandStatus {
    EXECUTING, // 执行中
    SUCCESS,   // 执行成功
    FAIL       // 执行失败
}
(2)定义交互DTO(前后端数据传输对象)
// 后端接收小程序请求的DTO
@Data
public class CommandProcessRequest {
    private String voiceText;  // 小程序传的语音指令文本
    private String userId;     // 用户ID(测试用固定值)
    private String aiModelType;// AI模型类型(测试用baidu)
}

// 后端返回给小程序的DTO
@Data
public class CommandProcessResponse {
    private String commandId;    // 指令ID
    private String deviceCode;   // 设备编码(如客厅灯=LT001)
    private String deviceName;   // 设备名称(如客厅主灯)
    private CommandType actionType; // 动作类型(OPEN/CLOSE)
    private LocalDateTime executedAt; // 执行时间
    private CommandStatus status;     // 指令状态
}
(3)定义统一返回结果(核心!新手必写)

前后端交互必须有统一的返回格式,否则小程序解析会混乱:

// 统一返回结果类
@Data
public class Result<T> {
    private int code;    // 状态码(200=成功,500=失败)
    private String msg;  // 提示信息(success/fail)
    private T data;      // 业务数据

    // 成功返回方法
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("success"); // 注意:小写,后续小程序要匹配
        result.setData(data);
        return result;
    }

    // 失败返回方法
    public static <T> Result<T> fail(String msg) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}
(4)编写Controller(接口入口)
@RestController
@RequestMapping("/api/commands")
@RequiredArgsConstructor
public class CommandController {

    // 模拟业务服务(新手可先注释,后续替换为真实逻辑)
    // private final CommandProcessApplicationService commandProcessApplicationService;

    /**
     * 小程序调用的核心接口:处理语音指令
     */
    @PostMapping("/process")
    public Result<CommandProcessResponse> processCommand(@RequestBody CommandProcessRequest request) {
        // 1. 参数校验(新手必做,避免空指针)
        if (request.getVoiceText() == null || request.getUserId() == null) {
            return Result.fail("语音文本和用户ID不能为空");
        }

        // 2. 模拟后端处理(真实场景替换为解析指令、调用设备逻辑)
        CommandProcessResponse response = new CommandProcessResponse();
        response.setCommandId(UUID.randomUUID().toString().replace("-", "")); // 随机指令ID
        response.setDeviceCode("LT001"); // 客厅灯编码
        response.setDeviceName("客厅主灯");
        response.setActionType(CommandType.OPEN); // 模拟解析为“打开”
        response.setExecutedAt(LocalDateTime.now());
        response.setStatus(CommandStatus.SUCCESS); // 模拟执行成功

        // 3. 返回统一结果
        return Result.success(response);
    }
}
(5)跨域配置(新手最容易忘的坑!)

小程序调用后端会触发跨域拦截,必须添加跨域配置:

@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 开发阶段允许所有来源(上线后改为指定域名)
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*"); // 允许所有请求头
        config.addAllowedMethod("*"); // 允许所有请求方法(GET/POST等)
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config); // 所有接口生效
        return new CorsFilter(source);
    }
}
(6)配置枚举序列化(避免返回对象)

Spring Boot默认会把枚举序列化为{name:"OPEN", ordinal:0},小程序无法解析,需配置返回字符串:

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
        return new Jackson2ObjectMapperBuilder()
                .modules(new SimpleModule()
                        // 所有枚举序列化时返回字符串(如OPEN/CLOSE)
                        .addSerializer(Enum.class, new ToStringSerializer()));
    }
}

3. 启动后端并测试

启动Spring Boot项目,默认端口8080(本文用8889,需在application.yml修改):

server:
  port: 8889 # 后端端口

启动后,在电脑浏览器访问http://localhost:8889,若显示“Whitelabel Error Page”(无404/无法访问),说明后端启动成功。

三、第二步:小程序前端开发(迭代1:基础结构)

1. 创建小程序项目

打开微信开发者工具,新建“不使用云服务”的小程序项目,选择“JavaScript-基础模板”。

2. 核心配置与代码

(1)页面结构(pages/voiceControl/voiceControl.wxml)
<view class="container">
  <!-- 录音按钮 -->
  <button bindtap="startVoiceControl" wx:if="{{!isRecording}}">按住录音</button>
  <button bindtap="startVoiceControl" wx:else>停止录音</button>
  
  <!-- 指令输入框(备用,测试用) -->
  <input placeholder="手动输入指令(如打开客厅灯)" bindinput="inputVoiceText" value="{{voiceText}}"></input>
  <button bindtap="executeVoiceCommand">执行指令</button>
  
  <!-- 结果提示 -->
  <view class="result" wx:if="{{showResult}}">{{resultText}}</view>
  
  <!-- 设备状态展示 -->
  <view class="device">
    <text>客厅主灯:</text>
    <text wx:if="{{deviceList.livingRoomLight.switch}}">打开</text>
    <text wx:else>关闭</text>
  </view>
</view>
(2)核心逻辑(pages/voiceControl/voiceControl.js)
Page({
  data: {
    voiceText: '',
    // 设备列表:补充code字段匹配后端设备编码(LT001/KT001)
    deviceList: {
      livingRoomLight: { 
        status: true, 
        switch: false, 
        code: 'LT001',    // 匹配后端客厅灯编码
        name: '客厅主灯'  
      },
      bedroomAir: { 
        status: true, 
        switch: true, 
        code: 'KT001',    // 匹配后端空调编码
        name: '卧室空调'  
      }
    },
    showResult: false,
    resultText: '',
    isRecording: false,
    recorderManager: null,
    tempFilePath: '',
    userId: 'wx_user_001', // 测试用固定用户ID
    baseApiUrl: 'http://192.168.1.3:8889', // 替换为你的电脑局域网IP
    commandId: ''
  },

  onLoad() {
    // 初始化录音管理器
    this.setData({
      recorderManager: wx.getRecorderManager()
    });
    this.initRecorder();
    // 启动时检测后端连接(方便排查)
    this.checkBackendConnection();
  },

  // 检测后端连接是否可用
  checkBackendConnection() {
    const that = this;
    wx.request({
      url: `${that.data.baseApiUrl}/api/commands/process`,
      method: 'POST',
      header: {
        'Content-Type': 'application/json; charset=utf-8'
      },
      data: { voiceText: '测试', userId: 'test', aiModelType: 'baidu' },
      timeout: 3000,
      success() {
        that.showResultToast('后端服务连接正常');
      },
      fail(err) {
        console.error('后端连接检测失败:', err);
        that.showResultToast('⚠️ 后端连接失败,请检查:\n1.后端是否启动\n2.IP/端口是否正确\n3.跨域配置是否生效');
      }
    });
  },

  // 初始化录音
  initRecorder() {
    const recorderManager = this.data.recorderManager;
    const that = this;

    // 录音开始回调
    recorderManager.onStart(() => {
      that.setData({ isRecording: true, resultText: '正在录音...' });
    });

    // 录音停止回调(获取临时文件路径)
    recorderManager.onStop((res) => {
      that.setData({
        isRecording: false,
        tempFilePath: res.tempFilePath
      });
      // 录音停止后,调用百度语音转文字
      that.voiceToText(res.tempFilePath);
    });

    // 录音错误回调
    recorderManager.onError((err) => {
      that.setData({ isRecording: false });
      that.showResultToast(`录音失败:${err.errMsg}`);
    });
  },

  // 开始/停止录音
  startVoiceControl() {
    const isRecording = this.data.isRecording;
    const recorderManager = this.data.recorderManager;

    if (isRecording) {
      recorderManager.stop(); // 停止录音
    } else {
      // 申请录音权限
      wx.authorize({
        scope: 'scope.record',
        success: () => {
          // 配置录音参数(百度语音识别要求wav格式)
          recorderManager.start({
            format: 'wav',
            sampleRate: 16000,
            numberOfChannels: 1,
            encodeBitRate: 64000,
            duration: 6000 // 最长录音6秒
          });
        },
        fail: () => {
          wx.showModal({
            title: '需要录音权限',
            content: '请前往设置开启麦克风权限',
            confirmText: '去设置',
            success: (res) => {
              if (res.confirm) wx.openSetting();
            }
          });
        }
      });
    }
  },

  // 手动输入指令
  inputVoiceText(e) {
    this.setData({
      voiceText: e.detail.value
    });
  },

  // 百度语音转文字
  voiceToText(tempFilePath) {
    const that = this;
    // 替换为你的百度API Key/Secret
    const apiKey = '你的百度API Key';
    const secretKey = '你的百度Secret Key';

    // 第一步:获取百度Access Token
    wx.request({
      url: 'https://aip.baidubce.com/oauth/2.0/token',
      method: 'POST',
      header: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      data: {
        grant_type: 'client_credentials',
        client_id: apiKey,
        client_secret: secretKey
      },
      success: (tokenRes) => {
        if (tokenRes.statusCode !== 200 || tokenRes.data.error) {
          const errMsg = tokenRes.data.error_description || 'Token获取失败';
          that.showResultToast(errMsg);
          return;
        }
        const accessToken = tokenRes.data.access_token;

        // 第二步:读取录音文件并转base64
        const fs = wx.getFileSystemManager();
        fs.readFile({
          filePath: tempFilePath,
          encoding: 'binary',
          success: (binaryRes) => {
            const fileBinary = binaryRes.data;
            const fileLen = fileBinary.length;
            // 二进制转base64(百度语音识别要求)
            const speechBase64 = wx.arrayBufferToBase64(new Uint8Array(fileBinary.split('').map(char => char.charCodeAt(0))));

            // 第三步:调用百度语音识别接口
            wx.request({
              url: 'https://vop.baidu.com/server_api',
              method: 'POST',
              header: {
                'Content-Type': 'application/json; charset=utf-8'
              },
              data: JSON.stringify({
                "format": "wav",
                "rate": 16000,
                "channel": 1,
                "cuid": "wx-miniprogram-123456789",
                "token": accessToken,
                "dev_pid": 1537, // 普通话识别
                "len": fileLen,
                "speech": speechBase64
              }),
              success: (transRes) => {
                console.log('百度语音识别结果:', transRes.data);
                if (transRes.data.err_no === 0 && transRes.data.result) {
                  const text = transRes.data.result[0];
                  that.setData({ voiceText: text });
                  that.showResultToast(`识别成功:${text}`);
                  // 识别成功后调用后端接口
                  that.callBackendCommandApi(text);
                } else {
                  const errMsg = transRes.data.err_msg || '识别失败';
                  that.showResultToast(`识别失败:${errMsg}(错误码:${transRes.data.err_no}`);
                }
              },
              fail: (err) => {
                console.error('语音识别请求失败:', err);
                that.showResultToast(`识别请求失败:${err.errMsg}`);
              }
            });
          },
          fail: (fileErr) => {
            console.error('读取录音文件失败:', fileErr);
            that.showResultToast(`读取录音失败:${fileErr.errMsg}`);
          }
        });
      },
      fail: (tokenErr) => {
        that.showResultToast(`获取Token失败:${tokenErr.errMsg}`);
      }
    });
  },

  // 调用后端接口处理指令
  callBackendCommandApi(voiceText) {
    const that = this;
    const { userId, baseApiUrl } = this.data;

    // 参数校验
    if (!userId) {
      that.showResultToast('用户未登录,请先登录');
      return;
    }
    if (!voiceText.trim()) {
      that.showResultToast('指令内容不能为空');
      return;
    }

    wx.showLoading({ title: '指令处理中...' });

    // 调用后端接口
    wx.request({
      url: `${baseApiUrl}/api/commands/process`,
      method: 'POST',
      header: {
        'Content-Type': 'application/json; charset=utf-8'
      },
      data: {
        voiceText: voiceText,
        userId: userId,
        aiModelType: 'baidu'
      },
      timeout: 5000,
      success: (res) => {
        wx.hideLoading();
        console.log('后端返回结果:', JSON.stringify(res.data));

        // 匹配后端统一返回格式(code=200且msg=success为成功)
        if (res.data && res.data.code === 200 && res.data.msg === 'success') {
          const commandResponse = res.data.data;
          that.setData({ commandId: commandResponse.commandId });
          // 同步设备状态
          that.syncDeviceStatusFromBackend(commandResponse);
        } else {
          const errMsg = res.data?.msg || '指令处理失败';
          that.showResultToast(`指令处理失败:${errMsg}`);
        }
      },
      fail: (err) => {
        wx.hideLoading();
        console.error('调用后端接口失败:', err);
        // 精准提示错误原因(新手友好)
        let errTips = '连接后端失败:';
        if (err.errMsg.includes('ERR_CONNECTION_REFUSED')) {
          errTips += '\n1. 后端服务未启动(8889端口)';
          errTips += '\n2. 确认IP是电脑局域网IP(192.168.1.3)';
          errTips += '\n3. 关闭电脑防火墙/检查跨域配置';
        } else if (err.errMsg.includes('timeout')) {
          errTips += '请求超时,请检查网络';
        } else {
          errTips += err.errMsg;
        }
        that.showResultToast(errTips);
      }
    });
  },

  // 同步后端返回的设备状态
  syncDeviceStatusFromBackend(commandResponse) {
    const that = this;
    const { deviceCode, actionType, status, deviceName } = commandResponse;

    // 匹配后端设备编码与本地设备(LT001 → livingRoomLight)
    let deviceKey = '';
    const deviceList = that.data.deviceList;
    for (const key in deviceList) {
      if (deviceList[key].code === deviceCode) {
        deviceKey = key;
        break;
      }
    }

    // 未匹配到设备
    if (!deviceKey) {
      that.showResultToast(`未识别设备:${deviceName || deviceCode}`);
      return;
    }

    // 解析动作(OPEN=打开,CLOSE=关闭)
    const isOpen = actionType === 'OPEN';
    const actionText = isOpen ? '打开' : '关闭';
    // 解析状态
    let statusText = '';
    switch (status) {
      case 'EXECUTING':
        statusText = '(执行中)';
        break;
      case 'SUCCESS':
        statusText = '(执行成功)';
        // 执行成功才更新本地设备状态
        const newDeviceList = { ...deviceList };
        newDeviceList[deviceKey].switch = isOpen;
        that.setData({ deviceList: newDeviceList });
        break;
      case 'FAIL':
        statusText = '(执行失败)';
        break;
      default:
        statusText = '';
    }

    // 提示结果
    that.showResultToast(`${deviceName || deviceList[deviceKey].name}${actionText}${statusText}`);
  },

  // 手动执行指令(测试用)
  executeVoiceCommand() {
    const { voiceText } = this.data;
    if (!voiceText.trim()) {
      this.showResultToast('请输入/说出控制指令');
      return;
    }
    this.callBackendCommandApi(voiceText);
  },

  // 结果提示框
  showResultToast(text) {
    this.setData({
      showResult: true,
      resultText: text
    });
    // 3秒后隐藏提示
    setTimeout(() => {
      this.setData({
        showResult: false
      });
    }, 3000);
  }
});
(3)关闭小程序域名校验(开发阶段)

新手容易被“合法域名”拦截,开发阶段临时关闭:

  1. 打开微信开发者工具 → 点击右上角「详情」;
  2. 在「本地设置」中,勾选「不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书」。

四、迭代2:联调踩坑与解决(核心!新手必看)

写完基础代码后,联调时会遇到各种问题,以下是我遇到的所有问题及解决方案,按“出现频率”排序:

坑1:小程序提示“连接后端失败:ERR_CONNECTION_REFUSED”

现象

小程序控制台打印:errMsg: "request:fail errcode:-102 cronet_error_code:-102 error_msg:net::ERR_CONNECTION_REFUSED"

原因
  • 后端服务未启动;
  • 后端端口被占用(8889);
  • 小程序baseApiUrl用了localhost(小程序模拟器/真机的localhost指向自身,而非电脑);
  • 电脑防火墙拦截了8889端口。
解决步骤
  1. 确认后端已启动:IDEA中启动Spring Boot项目,确保无报错;
  2. 检查端口占用:
    • Windows:CMD输入netstat -ano | findstr :8889,若有结果,关闭占用进程;
    • Mac/Linux:终端输入lsof -i :8889,kill占用进程;
  3. 替换baseApiUrl:将localhost改为电脑局域网IP(如192.168.1.3);
  4. 关闭电脑防火墙:
    • Windows:设置→更新和安全→Windows安全中心→防火墙和网络保护→关闭所有网络的防火墙;
    • Mac:系统设置→网络→防火墙→关闭。

坑2:小程序解析后端结果时,始终提示“指令处理失败”

现象

后端日志显示处理成功,但小程序提示“指令处理失败”。

原因
  • 小程序解析逻辑匹配success布尔值,但后端返回的是code=200+msg=success
  • msg大小写不匹配(后端返回success,小程序判断SUCCESS)。
解决步骤
  1. 修正小程序解析逻辑:将if (res.data.success)改为if (res.data && res.data.code === 200 && res.data.msg === 'success')
  2. 统一msg大小写:确保后端返回msg=success(小写),小程序也匹配小写。

坑3:真机测试提示“请求超时/无法连接”

现象

模拟器中能正常访问后端,但真机扫码后提示“连接后端失败:请求超时”。

原因
  • 真机和电脑未连同一Wi-Fi;
  • 电脑用了有线网络,小程序baseApiUrl填的是有线IP(真机连无线,无法访问);
  • 路由器开启了“设备隔离”,阻止设备间通信。
解决步骤
  1. 确保真机和电脑连同一个无线Wi-Fi(电脑切换为无线,不要用有线);
  2. 重新获取电脑的无线局域网IP(而非有线IP),替换baseApiUrl
  3. 关闭电脑防火墙(尤其是“公用网络防火墙”);
  4. 用手机浏览器测试:输入http://电脑无线IP:8889,能访问则网络正常。

坑4:设备状态无法同步(客厅灯开关不变)

现象

后端返回deviceCode=LT001,但小程序的客厅灯开关始终为“关闭”。

原因

小程序本地设备用livingRoomLight标识,后端用LT001,无映射关系。

解决步骤

在小程序deviceList中补充code字段,匹配后端设备编码:

deviceList: {
  livingRoomLight: { 
    status: true, 
    switch: false, 
    code: 'LT001', // 匹配后端LT001
    name: '客厅主灯'  
  }
}

坑5:后端返回的枚举是对象(而非字符串)

现象

小程序控制台打印actionType: {name:"OPEN", ordinal:0},无法解析。

原因

Spring Boot默认将枚举序列化为对象,小程序需要字符串。

解决步骤

添加JacksonConfig配置类(见后端开发部分),让枚举序列化返回字符串。

坑6:百度语音转文字失败(err_no=500)

现象

小程序提示“识别失败:server error(错误码:500)”。

原因
  • API Key/Secret错误;
  • 录音格式不匹配(百度要求wav/16000采样率);
  • 录音文件转base64错误。
解决步骤
  1. 核对百度API Key/Secret(确保未填错);
  2. 确认录音参数:format: 'wav'sampleRate: 16000
  3. 检查base64转换逻辑(代码中已封装,直接复用即可)。

五、迭代3:最终验证

完成所有坑的修复后,验证流程:

  1. 启动后端服务(IDEA);
  2. 小程序模拟器中输入“打开客厅灯”→ 点击“执行指令”;
  3. 控制台打印后端返回:
    {"code":200,"msg":"success","data":{"commandId":"xxx","deviceCode":"LT001","deviceName":"客厅主灯","actionType":"OPEN","executedAt":"2025-12-21T12:41:14.352356900","status":"SUCCESS"}}
    
  4. 小程序提示“客厅主灯打开(执行成功)”,客厅灯开关变为“打开”;
  5. 真机扫码测试:重复步骤2-4,功能正常。

六、新手建议(避坑总结)

  1. 网络优先:联调前先确保“电脑+真机同Wi-Fi”“关闭防火墙”“替换局域网IP”,80%的问题是网络导致;
  2. 日志为王:遇到问题先看控制台日志(小程序→Network/Console,后端→IDEA控制台),日志会明确提示错误原因;
  3. 统一格式:前后端约定好统一的返回格式(如code+msg+data),避免字段匹配混乱;
  4. 分步测试:先测试后端接口(Postman),再测试小程序调用后端,最后测试完整流程,分步定位问题;
  5. 测试完成后恢复配置:开启电脑防火墙、取消小程序域名校验勾选,避免安全风险。

七、完整代码仓库(可选)

为方便新手复用,我将完整的后端+前端代码整理到了Gitee(新手友好,带注释):

(注:“代码已在文中完整贴出,直接复制即可”)

总结

小程序与后端交互的核心是“网络连通+数据格式匹配”:网络问题靠“同Wi-Fi+局域网IP+关闭防火墙”解决,数据问题靠“统一返回格式+字段映射”解决。作为新手,不用怕踩坑——每一个坑都是对“网络通信”“数据交互”的理解加深。跟着本文的迭代步骤,从基础代码到踩坑解决,你能完整掌握小程序与后端交互的核心逻辑。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Coder_Boy_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值