springboot + springAi+阿里云百炼平台+deepseek,在业务系统中接入ai智能体,实现对业务方面的问答

一、前言

目前市面上接入ai的方式主要有三种:

  • 调用api接口。
    缺点:功能简陋,如果需要实现更多的功能需要自主封装。
  • 使用各产商提供的sdk。
    优点:开箱即用,实现简单,功能性强。
    缺点:各产商都有自己的一套规范,如果需要接入多家的ai,得阅读多家的文档。
  • 使用springAi。
    优点:完美契合Spring生态,实现简单,功能性强。
    缺点:某些ai未做适配。

推荐优先级:springAi>sdk>api。

本文介绍了使用Spring AI框架深度集成业务系统的方法,实现本地化AI问答功能。主要内容包括:

  1. 技术选型:基于SpringBoot框架,使用Spring AI接入阿里云百炼deepseek-r1文本模型,配合Elasticsearch向量数据库和Redis缓存。
  2. 实现功能:
    流式输出响应
    多轮对话上下文管理
    对话中断处理
    完整的对话日志记录
  3. 系统特点:
    通过向量数据库和业务数据增强AI知识库
    提供配置化的相似度阈值和上下文窗口大小
    实现Redis缓存的对话记忆持久化
  4. 实施建议:采用Spring AI优先的接入方案,相比直接调用API或SDK更易于与Spring生态集成。

文章包含完整的代码示例和配置说明(基本每一行代码和配置都有说明,作用是什么,为什么要这样写),适合开发者快速实现业务场景的AI对话功能。

二、使用技术

基础框架:springboot
ai框架:springAi
jdk:17
文本模型:阿里云百炼deepseek-r1
向量模型:text-embedding-v4
向量数据库:elasticsearch
缓存数据库:redis

三、代码实现

1、导入maven坐标

	<dependencies>	
		<!-- 使用的ruoyi框架,其它模块依赖,使用到租户模块、鉴权模块、web模块、mybatis模块,可以改造或删除 -->
		<dependency>
            <groupId>org.dromara</groupId>
            <artifactId>ruoyi-common-tenant</artifactId>
        </dependency>
        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>ruoyi-common-satoken</artifactId>
        </dependency>
        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>ruoyi-common-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.dromara</groupId>
            <artifactId>ruoyi-common-mybatis</artifactId>
        </dependency>
		<!-- 工具依赖 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-json</artifactId>
            <version>5.8.35</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.12</version>
        </dependency>
        <!-- spring-ai 核心依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-elasticsearch-store-spring-boot-starter</artifactId>
        	<version>1.0.0-M6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud.ai</groupId>
            <artifactId>spring-ai-alibaba-starter</artifactId>
            <version>1.0.0-M6.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.17.0</version>
        </dependency>
  </dependencies>

2、yml配置

spring:
#elasticsearch向量数据库配置
  elasticsearch:
    uris: http://127.0.0.1:9200
    # 本地elasticsearch未开启认证,生产环境必须开启
    username: root
    password: famsakfdsfx
spring:
  ai:
    dashscope:
      api-key: sk-c7********************* #从阿里云百炼平台获取
      model: deepseek-r1 #文本模型名称
      embedding:
        options:
          # 嵌入的向量模型
          model: text-embedding-v4
    vectorstore: #向量相关配置
      elasticsearch: #使用elasticsearch作为向量数据库,也可以使用其他的,如redis向量数据库
        initialize-schema: true #如果不存在该索引则创建
        index-name: spring-ai-rag #索引名称(这里的索引并不是mysql那种索引)
        # elasticsearch的向量维度(默认1536),需要和向量模型的维度一致。因为text-embedding-v4向量模型配置维度不生效,只能配置默认的1024
        dimensions: 1024
        similarity: cosine #相似配置枚举类型
    #以上是springAi自身的配置,以下是自定义配置
    # ai初始化配置
    client:
      enabled: false #是否开启初始化
      # 系统的默认行为和风格
      defaultSystem: 你是一个电子产品回收系统专业的助手,遇到不确定或不明确的信息时,会主动询问用户以获取更多信息。在回答问题时,你倾向于使用条理清晰的格式,例如分点列举的方式,以便用户更容易理解和参考。同时,你拒绝回答与本系统无关的问题。
      # 相似度阈值,取值区间:(0,1]
      similarity-threshold: 0.3
      # 返回匹配的向量文档数量,可以按每个向量文档的token大小来设置,token大,topK则设置小一些,否则会超出ai对话的token长度限制
      topK: 50
      # 对话上下文记忆条数
      chatHistoryWindowSize: 20

3、ai初始化及配置类

3.1、ai对话配置信息

读取yml配置

/**
 * ai对话配置信息
 */
@Data
@Configuration
@ConfigurationProperties("spring.ai.client")
public class ChatClientConfigInfo {

    /**
     * 是否开启ai知识库初始化(原本是设计成是否开启ai的,后续会更新)
     */
    private Boolean enabled;

    /**
     * 系统的默认行为和风格
     */
    private String defaultSystem;

    /**
     * 相似度阈值,取值区间:(0,1]
     */
    private Double similarityThreshold;

    /**
     * 返回匹配的向量文档数量,并不是越多越好,ai对话的token是有上限的
     * 可以按每个向量文档的token大小来设置,token大,topK则设置小一些,否则会超出ai对话的token长度限制
     */
    private Integer topK;

    /**
     * 对话上下文记忆条数
     */
    private Integer chatHistoryWindowSize;

    /**
     * 提示词(问题引导,引导用户怎么提问。这个可以作为ai初始界面的问题引导或用户提问与系统业务无关时候返回的引导)
     */
    private List<String> issues;
}
3.2、对话上下文缓存(实现ChatMemory)

为什么要缓存对话记录?因为ai是没有记忆的,想要实现多轮对话,需要将之前的对话信息一并告诉它。
实现该功能的核心是实现ChatMemory接口并注册成Spring Bean。以下是以redis作为缓存,同时将对话信息持久化。

@Slf4j
@Component
@RequiredArgsConstructor
public class ChatRedisMemory implements ChatMemory {
    private static final String KEY_PREFIX = "chat:history:";
    private final IChatAiLogService chatAiLogService;

	//conversationId:缓存标识,本文使用的是用户ID
    @Override
    public void add(String conversationId, Message message) {
        ChatMemory.super.add(conversationId, message);
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
        String key = KEY_PREFIX + conversationId;
        List<ChatEntity> listIn = new ArrayList<>();
        List<ChatAiLog> listLog = new ArrayList<>();
        for (Message msg : messages) {
            //去除think过程
            String[] strs = msg.getText().split("</think>");
            String text = strs.length == 2 ? strs[1] : strs[0];
            // 构建缓存实体
            ChatEntity ent = new ChatEntity();
            ent.setChatId(conversationId);
            ent.setType(msg.getMessageType().getValue());
            ent.setText(text);
            listIn.add(ent);
            // 构建持久化实体
            //缓存实体和持久化实体就相差一个字段,之所以要分开,是因为缓存信息的要作为上下文提供给ai的,越简练越好
            ChatAiLog chatAiLog = new ChatAiLog();
            chatAiLog.setChatId(conversationId);
            chatAiLog.setType(msg.getMessageType().getValue());
            chatAiLog.setText(text);
            chatAiLog.setChatTime(DateUtils.getNowDate());
            listLog.add(chatAiLog);
        }
        //redis的工具类,可以使用自身框架的,或自主实现
        // 缓存对话记录并设置超时时间
        RedisUtils.setCacheList(key, listIn);
        RedisUtils.expire(key, Duration.ofMinutes(30));
        // 对话日志持久化
        chatAiLogService.saveBatch(listLog);
    }

    // lastN:获取取上下文的条数,怎么传进来的后面代码中有使用
    @Override
    public List<Message> get(String conversationId, int lastN) {
        String key = KEY_PREFIX + conversationId;
        List<Message> listOut = new ArrayList<>();
        // 校验缓存是否存在
        if (!RedisUtils.hasKey(key)) {
            return listOut;
        }
        long size = RedisUtils.getCacheList(key).size();
        if (size == 0) {
            return listOut;
        }

        int start = Math.max(0, (int) (size - lastN));
        List<Object> listTmp = RedisUtils.getCacheListRange(key, start, -1);
        ObjectMapper objectMapper = new ObjectMapper();
        for (Object obj : listTmp) {
            ChatEntity chat = objectMapper.convertValue(obj, ChatEntity.class);
            //构建ai需要的数据格式,MessageType是对话角色:user是用户,system是ai,还有两个特殊的对话角色:assistant(对话组手,如提示语)、ai工具(tool)本文未记录该角色的对话信息
            if (MessageType.USER.getValue().equals(chat.getType())) {
                listOut.add(new UserMessage(chat.getText()));
            } else if (MessageType.ASSISTANT.getValue().equals(chat.getType())) {
                listOut.add(new AssistantMessage(chat.getText()));
            } else if (MessageType.SYSTEM.getValue().equals(chat.getType())) {
                listOut.add(new SystemMessage(chat.getText()));
            }
        }

        return listOut;
    }

    @Override
    public void clear(String conversationId) {
        RedisUtils.deleteObject(KEY_PREFIX + conversationId);
    }
}

对话实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatEntity implements Serializable {
    /**
     * 对话ID
     */
    private String chatId;

    /**
     * 类型(system、user、assistant、tool)
     */
    private String type;

    /**
     * 对话消息
     */
    private String text;
}
3.3、ai初始化
@Slf4j
@Configuration
@RequiredArgsConstructor
public class ChatClientConfig {

    private final ChatClientConfigInfo chatClientConfigInfo;
    private final VectorStore vectorStore;
    private final ChatModel chatModel;
    private final IChatAiLogService chatAiLogService;

    /**
     * 初始化AI
     */
    @Bean
    public ChatClient chatClient() {
        return ChatClient.builder(chatModel)
        // 系统的默认行为和风格,怎么描述需要自主调试,才能达到想要的效果
            .defaultSystem(chatClientConfigInfo.getDefaultSystem())
            // 默认的顾问(增强ai的一种手段,这里默认只配置向量检索,还可以配置日志等等,想要增强哪方面,可以在springAi官网文档中查看。)
            //本文还使用了对话信息顾问,即上下文检索,在调用时使用。
            .defaultAdvisors(
                // 注册向量检索顾问,用于从向量存储中检索相关信息
                new QuestionAnswerAdvisor(
                //向量存储器,有4个继承类,本文使用elasticsearch作为向量数据库,所以默认是使用elasticsearch的实现
                    vectorStore,
                    //向量文档的检索配置,需要自主微调
                    SearchRequest.builder()
                    	//相似度阈值
                        .similarityThreshold(chatClientConfigInfo.getSimilarityThreshold())
                        //返回匹配的向量文档数量
                        .topK(chatClientConfigInfo.getTopK())
                        .build()
                )
            )
            .build();
    }

    /**
     * 注册对话上下文缓存
     */
    @Bean
    public ChatMemory chatMemory() {
        return new ChatRedisMemory(chatAiLogService);
    }

}
3.4、初始化知识库

将准备好的基本信息文件转化成向量文档存储到向量数据库中,在使用ai时检索与问题相似数据提供给ai。初始化信息可以是:系统介绍、公司介绍、操作手册、业务名称说明、业务表结构说明等等,文件类型推荐文本和word文档。

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SpringAiRunner implements ApplicationRunner {

    private final static String knowledgeBasePath = "chatai/knowledgeBase";
    private final static String issuePath = "chatai/问题提示语.txt";
    private final ChatClientConfigInfo chatClientConfigInfo;
    private final VectorStoreService vectorStoreService;

    @Override
    public void run(ApplicationArguments args) {
        if (chatClientConfigInfo.getEnabled()){
            knowledgeBaseInit();
        }
        readIssue();
    }

    /**
     * 初始化知识库
     */
    public void knowledgeBaseInit() {
        List<String> fileNames = new ArrayList<>();
        try {
            fileNames = FileUtils.getClassPathResourceFileName(knowledgeBasePath);
        } catch (IOException e) {
            log.info("无初始化知识库文件!");
        }

        // 删除旧文档和添加新文档
        vectorStoreService.deleteIn(VectorKey.FILE,fileNames);
        for (String fileName : fileNames) {
            String filePath = knowledgeBasePath + "/" + fileName;
            vectorStoreService.saveFilePath(fileName,filePath);
        }
        log.info("初始化 向量知识库 成功!");
    }

    /**
     * 读取【问题提示词.txt】文件添加到chatClientConfigInfo
     */
    public void readIssue(){
        try {
            List<String> issues = FileUtils.readLinesFromFile(issuePath);
            List<String> infoIssues = chatClientConfigInfo.getIssues();
            if (infoIssues != null){
                issues.addAll(infoIssues);
            }
            chatClientConfigInfo.setIssues(issues);
         } catch (IOException e) {
            log.info("读取【{}】文件失败",issuePath);
        }
    }

}

例:
文件示例
系统介绍示例

4、对话实现

由于ai功能模块化,为了避免耦合,由ai模块提供接口给业务模块,业务模块实现接口,传入ai工具(tool)到ai模块,由ai模块实现对话的核心逻辑。这里是唯一与业务模块关联的地方。

4.1、控制层(ai模块)
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat/v1")
public class ChatController {

    private final ChatService chatService;
    private final ChatMemory chatMemory;

    /**
     * deepSeek
     */
    @GetMapping(value = "/deepseek/rag")
    public Flux<String> ragChat(String message) {
        return chatService.ragChat(message);
    }

    /**
     * 清空上下文
     */
    @GetMapping(value = "/deepseek/clearAway")
    public R<Void> clearAway() {
        chatMemory.clear(LoginHelper.getUserId().toString());
        return R.ok();
    }

	/**
     * 中断对话(后续实现)
     */
}
4.2、ai服务接口(ai模块)

提供给外部的接口,业务模块实现该接口,本文是单例实现,可以改造成策略模式达到多实现的目的,使ai更加专注于某一块业务的处理。

/**
 * ai服务
 */
public interface ChatService {

    /**
     * 多轮对话
     */
    Flux<String> ragChat(String message);

}
4.3、实现ai服务接口(业务模块)
/**
 * 回收报价ai服务
 */
@Service
@RequiredArgsConstructor
public class EsChatService implements ChatService {

	//ai工具调用类
    private final EquipmentTool equipmentTool;
    private final OrderTool orderTool;
    
    private final ChatClientService chatClientService;

    @Override
    public Flux<String> ragChat(String message) {
    	//主要目的:根据业务需要选择性传入ai工具(tool)。
        return chatClientService.send(message, equipmentTool, orderTool);
    }

}
4.4、ai工具(tool)(业务模块)

核心内容:@Tool和@ToolParam注解,其他可有可无,看业务需要。
只要在方法上加了@Tool注解,那该方法即是ai工具(tool)。
@Tool:使用自然语言描述标记的方法的用处,当ai根据对话内容匹配到时候就会执行该方法。
@ToolParam:使用自然语言描述标记的参数,由ai根据对话内容自动传入。
**作用:**作为给ai提供数据的一种方法,或是由ai操作业务的方法,如在订票系统中:帮我订一张xxx的票。(让ai操作业务需要谨慎,ai并不是百分百准确的)
以下为@Tool的使用示例,与ai模块无关。

@Slf4j
@Component
@RequiredArgsConstructor
@Description("查询回收设备及价格服务")
public class EquipmentTool {

    private final EsEquipmentMapper equipmentMapper;
    private final EsPriceMapper priceMapper;

    @Tool(description = "查询可回收的产品、设备或型号")
    public List<Map<String, String>> productName(@ToolParam(description = "类型") String equipmentType,
                                                     @ToolParam(description = "品牌") String brand,
                                                     @ToolParam(description = "系列", required = false) String series,
                                                     @ToolParam(description = "型号", required = false) String model,
                                                     @ToolParam(description = "内存", required = false) String internalStorage) {
        return equipmentMapper.getProductNameList(equipmentType, brand, series, model, internalStorage);
    }

    @Tool(description = "查询价格、回收价格")
    public List<Map<String, String>> selectPrice(@ToolParam(description = "类型", required = false) String equipmentType,
                                                     @ToolParam(description = "品牌", required = false) String brand,
                                                     @ToolParam(description = "系列", required = false) String series,
                                                     @ToolParam(description = "型号") String model,
                                                     @ToolParam(description = "内存", required = false) String internalStorage,
                                                     @ToolParam(description = "成色", required = false) String fineness) {
        return priceMapper.selectPriceListByTableName("es_price",equipmentType, brand, series, model, internalStorage, fineness);
    }

    @Tool(description = "查询最新价格、回收价格")
    public List<Map<String, String>> selectNewPrice(@ToolParam(description = "类型", required = false) String equipmentType,
                                                     @ToolParam(description = "品牌", required = false) String brand,
                                                     @ToolParam(description = "系列", required = false) String series,
                                                     @ToolParam(description = "型号") String model,
                                                     @ToolParam(description = "内存", required = false) String internalStorage,
                                                     @ToolParam(description = "成色", required = false) String fineness) {
        return priceMapper.selectPriceListByTableName("es_price",equipmentType, brand, series, model, internalStorage, fineness);
    }

    @Tool(description = "查询历史价格、回收价格")
    public List<Map<String, String>> selectHistoryPrice(@ToolParam(description = "类型", required = false) String equipmentType,
                                                            @ToolParam(description = "品牌", required = false) String brand,
                                                            @ToolParam(description = "系列", required = false) String series,
                                                            @ToolParam(description = "型号") String model,
                                                            @ToolParam(description = "内存", required = false) String internalStorage,
                                                            @ToolParam(description = "成色", required = false) String fineness,
                                                            @ToolParam(description = "日期", required = false) Date date) {
        String dateToStr = DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_, date);
        String tableName = String.format("%s_%s", "es_price", dateToStr);
        return priceMapper.selectPriceListByTableName(tableName,equipmentType, brand, series, model, internalStorage, fineness);
    }

}
4.5、对话核心实现(ai模块)
@Service
@RequiredArgsConstructor
public class ChatClientService {

    private final ChatClientConfigInfo chatClientConfigInfo;
    private final VectorStoreService vectorStoreService;
    private final ChatClient chatClient;
    private final ChatMemory chatMemory;

    /**
     * 发送对话
     *
     * @param message     对话信息
     * @param toolObjects ai工具(tool)
     */
    public Flux<String> send(String message, Object... toolObjects) {
        String userId;
        try {
            userId = LoginHelper.getLoginUser().getUserId().toString();
        } catch (Exception e) {
            return Flux.just("请先登录!");
        }
        //先进行数据检索,在知识库中未检索到信息则认为是与本系统无关的问题
        List<Document> documentList = vectorStoreService.search(message);
        if (documentList.isEmpty()) {
        // 这里是获取3.4初始化知识库中的问题提示词,引导用户提问方式。
            List<String> issues = chatClientConfigInfo.getIssues();
            Collections.shuffle(issues);
            return Flux.just("我无法回答与系统无关的问题,请换一个问题吧。" +
                "\n" + issues.get(0) + "\n" + issues.get(1) + "\n" + issues.get(2)
            );
        }
        return chatClient.prompt()
        // 设置用户对话
            .user(message)
            // 上下文顾问
            .advisors(new MessageChatMemoryAdvisor(
            // 添加3.2中实现的redis上下文缓存
            chatMemory, 
            // 标识,这里使用的是用户ID,对用户的对话进行隔离
            // 如有多人追问ai的场景需求的,可以使用组ID、部门ID等等
            userId, 
            //对话上下文记忆条数
            chatClientConfigInfo.getChatHistoryWindowSize()))
            // 添加ai工具(toolObjects)
            .tools(toolObjects)
            // 使用流式输出
            .stream().content();
    }

}

5、向量数据的存储与检索(核心:VectorStore类)

VectorStore类是没有修改方法的,只有新增和删除。
目前在做多租户数据隔离改造,后续更新。
功能有:向量数据库的查询、新增、批量新增(需要额外编写配置类)、删除、条件过滤。

6、ai对话日志

简单日志信息的存储和查询,常用的三层架构,可以改成其他的方式。
ai对话日志是为了通过分析更好的调整ai,如用户高频问题,可以编写专门的ai工具调用方法。

6.1、实体类
/**
 * ai对话日志
 */
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("chat_ai_log")
public class ChatAiLog  extends BaseEntity {

    @Serial
    private static final long serialVersionUID = 1L;

    @TableId(value = "id",type = IdType.AUTO)
    private Long id;

    /**
     * 对话ID(用户ID)
     */
    private String chatId;

    /**
     * 类型(system、user、assistant)
     */
    private String type;

    /**
     * 对话消息
     */
    private String text;

    /**
     * 对话时间
     */
    private Date chatTime;

}
6.2、Mapper及xml文件
public interface ChatAiLogMapper extends BaseMapper<ChatAiLog> {
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.chatai.mapper.ChatAiLogMapper">

</mapper>
6.3、service及impl
public interface IChatAiLogService extends IService<ChatAiLog> {

    TableDataInfo<ChatAiLog> queryPageList(ChatAiLog bo, PageQuery pageQuery);

}


@Slf4j
@RequiredArgsConstructor
@Service
public class IChatAiLogServiceImpl  extends ServiceImpl<ChatAiLogMapper, ChatAiLog> implements IChatAiLogService {

    @Override
    public TableDataInfo<ChatAiLog> queryPageList(ChatAiLog bo, PageQuery pageQuery) {
        LambdaQueryWrapper<ChatAiLog> lqw = buildQueryWrapper(bo);
        Page<ChatAiLog> result = baseMapper.selectPage(pageQuery.build(), lqw);
        return TableDataInfo.build(result);
    }

    private LambdaQueryWrapper<ChatAiLog> buildQueryWrapper(ChatAiLog bo) {
        LambdaQueryWrapper<ChatAiLog> lqw = Wrappers.lambdaQuery();
        lqw.orderByAsc(ChatAiLog::getId);
        lqw.like(StringUtils.isNotBlank(bo.getType()), ChatAiLog::getType, bo.getType());
        lqw.eq(StringUtils.isNotBlank(bo.getText()), ChatAiLog::getText, bo.getText());
        return lqw;
    }

}

6.4、controller
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat/log")
public class ChatAiLogController {

    private final IChatAiLogService chatAiLogService;

    /**
     * 查询ai对话日志列表
     */
    @SaCheckPermission("chat:log:list")
    @GetMapping("/list")
    public TableDataInfo<ChatAiLog> list(ChatAiLog bo, PageQuery pageQuery) {
        return chatAiLogService.queryPageList(bo, pageQuery);
    }

    /**
     * 获取ai对话日志详细信息
     *
     * @param id 主键
     */
    @SaCheckPermission("chat:log:query")
    @GetMapping("/{id}")
    public R<ChatAiLog> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
        return R.ok(chatAiLogService.getById(id));
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值