几百行代码完成百度搜索引擎,真的可以用吗?

《vite技术揭秘、还原与实战》第8节--插件化方案设计 我们总是说“程序设计与实现”,之所以把设计放在前,是因为它能避免我们像无头苍蝇一样随遇而安如果我们接着前边几章继续往下写,那大概率是这样的开发思路:在 http 服务器中监听由浏览器解析后抛出的文件请求,然后去读取文件源码,最后做一定的处理转换后返还给浏览器客户端如果 vite 是一个面向普通用户提供服务的产品的话,闭塞似乎没有问题。可它面向的是开发人员,定位是工具。因此必须要考虑到开发者的客制化需求,而插件机制是提供这一能力的最优解好文推荐vite技术揭秘、还原与实战-专栏-每周更新。 阅读详情

推荐阅读:

搜索引擎想必大家一定不会默认,我们项目中经常使用的 ElasticSearch 就是一种搜索引擎,在我们的日志系统中必不可少,ELK 作为一个整体,基本上是运维标配了,另外目前的搜索引擎底层都是基于 Lucene 来实现的。

小编最近遇到一个需求,因为数据量没有达到需要使用 ElasticSearch 的级别,也不想单独部署一套集群,所以准备自己基于 Lucene 实现一个简易的搜索服务。下面我们一起来看一下吧。

背景

Lucene是一套用于全文检索和搜索的开放源码程序库,由 Apache 软件基金会支持和提供。Lucene 提供了一个简单却强大的应用程序接口,能够做全文索引和搜索。Lucene 是现在最受欢迎的免费 Java 信息检索程序库。

上面的解释是来自维基百科,我们只需要知道 Lucene 可以进行全文索引和搜索就行了,这里的索引是动词,意思是我们可以将文档或者文章或者文件等数据进行索引记录下来,索引过后,我们查询起来就会很快。

索引这个词有的时候是动词,表示我们要索引数据,有的时候是名词,我们需要根据上下文场景来判断。新华字典前面的字母表或者书籍前面的目录本质上都是索引。

接入

引入依赖

首先我们创建一个 SpringBoot 项目,然后在 pom 文件中加入如下内容,我这里使用的 lucene 版本是 7.2.1,

<properties>
    <lucene.version>7.2.1</lucene.version>
</properties>

<!-- Lucene核心库 -->
<dependency>
 <groupId>org.apache.lucene</groupId>
 <artifactId>lucene-core</artifactId>
 <version>${lucene.version}</version>
</dependency>
<!-- Lucene解析库 -->
<dependency>
 <groupId>org.apache.lucene</groupId>
 <artifactId>lucene-queryparser</artifactId>
 <version>${lucene.version}</version>
</dependency>
<!-- Lucene附加的分析库 -->
<dependency>
 <groupId>org.apache.lucene</groupId>
 <artifactId>lucene-analyzers-common</artifactId>
 <version>${lucene.version}</version>
</dependency>

索引数据

在使用 Lucene 之前我们需要先索引一些文件,然后再通过关键词查询出来,下面我们来模拟整个过程。为了方便我们这里模拟一些数据,正常的数据应该是从数据库或者文件中加载的,我们的思路是这样的:

  1. 生成多条实体数据;
  2. 将实体数据映射成 Lucene 的文档形式;
  3. 索引文档;
  4. 根据关键词查询文档;

第一步我们先创建一个实体如下:

import lombok.Data;

@Data
public class ArticleModel {
    private String title;
    private String author;
    private String content;
}

我们再写一个工具类,用来索引数据,代码如下:

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class LuceneIndexUtil {

    private static String INDEX_PATH = "/opt/lucene/demo";
    private static IndexWriter writer;

    public static LuceneIndexUtil getInstance() {
        return SingletonHolder.luceneUtil;
    }

    private static class SingletonHolder {
        public final static LuceneIndexUtil luceneUtil = new LuceneIndexUtil();
    }

    private LuceneIndexUtil() {
        this.initLuceneUtil();
    }

    private void initLuceneUtil() {
        try {
            Directory dir = FSDirectory.open(Paths.get(INDEX_PATH));
            Analyzer analyzer = new StandardAnalyzer();
            IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
            writer = new IndexWriter(dir, iwc);
        } catch (IOException e) {
            log.error("create luceneUtil error");
            if (null != writer) {
                try {
                    writer.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                } finally {
                    writer = null;
                }
            }
        }
    }

    /**
     * 索引单个文档
     *
     * @param doc 文档信息
     * @throws IOException IO 异常
     */
    public void addDoc(Document doc) throws IOException {
        if (null != doc) {
            writer.addDocument(doc);
            writer.commit();
            writer.close();
        }
    }

    /**
     * 索引单个实体
     *
     * @param model 单个实体
     * @throws IOException IO 异常
     */
    public void addModelDoc(Object model) throws IOException {
        Document document = new Document();
        List<Field> fields = luceneField(model.getClass());
        fields.forEach(document::add);
        writer.addDocument(document);
        writer.commit();
        writer.close();
    }

    /**
     * 索引实体列表
     *
     * @param objects 实例列表
     * @throws IOException IO 异常
     */
    public void addModelDocs(List<?> objects) throws IOException {
        if (CollectionUtils.isNotEmpty(objects)) {
            List<Document> docs = new ArrayList<>();
            objects.forEach(o -> {
                Document document = new Document();
                List<Field> fields = luceneField(o);
                fields.forEach(document::add);
                docs.add(document);
            });
            writer.addDocuments(docs);
        }
    }

    /**
     * 清除所有文档
     *
     * @throws IOException IO 异常
     */
    public void delAllDocs() throws IOException {
        writer.deleteAll();
    }

    /**
     * 索引文档列表
     *
     * @param docs 文档列表
     * @throws IOException IO 异常
     */
    public void addDocs(List<Document> docs) throws IOException {
        if (CollectionUtils.isNotEmpty(docs)) {
            long startTime = System.currentTimeMillis();
            writer.addDocuments(docs);
            writer.commit();
            log.info("共索引{}个 Document,共耗时{} 毫秒", docs.size(), (System.currentTimeMillis() - startTime));
        } else {
            log.warn("索引列表为空");
        }
    }

    /**
     * 根据实体 class 对象获取字段类型,进行 lucene Field 字段映射
     *
     * @param modelObj 实体 modelObj 对象
     * @return 字段映射列表
     */
    public List<Field> luceneField(Object modelObj) {
        Map<String, Object> classFields = ReflectionUtils.getClassFields(modelObj.getClass());
        Map<String, Object> classFieldsValues = ReflectionUtils.getClassFieldsValues(modelObj);

        List<Field> fields = new ArrayList<>();
        for (String key : classFields.keySet()) {
            Field field;
            String dataType = StringUtils.substringAfterLast(classFields.get(key).toString(), ".");
            switch (dataType) {
                case "Integer":
                    field = new IntPoint(key, (Integer) classFieldsValues.get(key));
                    break;
                case "Long":
                    field = new LongPoint(key, (Long) classFieldsValues.get(key));
                    break;
                case "Float":
                    field = new FloatPoint(key, (Float) classFieldsValues.get(key));
                    break;
                case "Double":
                    field = new DoublePoint(key, (Double) classFieldsValues.get(key));
                    break;
                case "String":
                    String string = (String) classFieldsValues.get(key);
                    if (StringUtils.isNotBlank(string)) {
                        if (string.length() <= 1024) {
                            field = new StringField(key, (String) classFieldsValues.get(key), Field.Store.YES);
                        } else {
                            field = new TextField(key, (String) classFieldsValues.get(key), Field.Store.NO);
                        }
                    } else {
                        field = new StringField(key, StringUtils.EMPTY, Field.Store.NO);
                    }
                    break;
                default:
                    field = new TextField(key, JsonUtils.obj2Json(classFieldsValues.get(key)), Field.Store.YES);
                    break;
            }
            fields.add(field);
        }
        return fields;
    }
    public void close() {
        if (null != writer) {
            try {
                writer.close();
            } catch (IOException e) {
                log.error("close writer error");
            }
            writer = null;
        }
    }

    public void commit() throws IOException {
        if (null != writer) {
            writer.commit();
            writer.close();
        }
    }
}

有了工具类,我们再写一个 demo 来进行数据的索引

import java.util.ArrayList;
import java.util.List;

/**
 * <br>
 * <b>Function:</b><br>
 * <b>Author:</b>@author Silence<br>
 * <b>Date:</b>2020-10-17 21:08<br>
 * <b>Desc:</b>无<br>
 */
public class Demo {
    public static void main(String[] args) {
        LuceneIndexUtil luceneUtil = LuceneIndexUtil.getInstance();
        List<ArticleModel> articles = new ArrayList<>();
        try {
            //索引数据
            ArticleModel article1 = new ArticleModel();
            article1.setTitle("Java天下第一");
            article1.setAuthor("粉丝");
            article1.setContent("这是一篇给大家介绍 Lucene 的技术文章,必定点赞评论转发!!!");
            ArticleModel article2 = new ArticleModel();
            article2.setTitle("天下第一");
            article2.setAuthor("粉丝");
            article2.setContent("此处省略两千字...");
            ArticleModel article3 = new ArticleModel();
            article3.setTitle("Java天下第一");
            article3.setAuthor("粉丝");
            article3.setContent("Today is big day!");
            articles.add(article1);
            articles.add(article2);
            articles.add(article3);
            luceneUtil.addModelDocs(articles);
            luceneUtil.commit();
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

上面的 content 内容可以自行进行替换,小编这边避免凑字数的嫌疑就不贴了。

展示

运行结束过后,我们用过 Lucene 的可视化工具 luke 来查看下索引的数据内容,下载过后解压我们可以看到有.bat 和 .sh 两个脚本,根据自己的系统进行运行就好了。小编这边是 mac 用的是 sh 脚本运行,运行后打开设置的索引目录即可。

进入过后,我们可以看到下图显示的内容,选择 content 点击 show top items 可以看到右侧的索引数据,这里根据分词器的不同,索引的结果是不一样的,小编这里采用的分词器就是标准的分词器,小伙伴们可以根据自己的要求选择适合自己的分词器即可。

搜索数据

数据已经索引成功了,接下来我们就需要根据条件进行数据的搜索了,我们创建一个 LuceneSearchUtil.java 来操作数据。

import org.apache.commons.collections.MapUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.springframework.beans.factory.annotation.Value;

import java.io.IOException;
import java.nio.file.Paths;
import java.util.Map;


public class LuceneSearchUtil {

    private static String INDEX_PATH = "/opt/lucene/demo";
    private static IndexSearcher searcher;

    public static LuceneSearchUtil getInstance() {
        return LuceneSearchUtil.SingletonHolder.searchUtil;
    }

    private static class SingletonHolder {
        public final static LuceneSearchUtil searchUtil = new LuceneSearchUtil();
    }

    private LuceneSearchUtil() {
        this.initSearcher();
    }

    private void initSearcher() {
        Directory directory;
        try {
            directory = FSDirectory.open(Paths.get(INDEX_PATH));
            DirectoryReader reader = DirectoryReader.open(directory);
            searcher = new IndexSearcher(reader);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public TopDocs searchByMap(Map<String, Object> queryMap) throws Exception {
        if (null == searcher) {
            this.initSearcher();
        }
        if (MapUtils.isNotEmpty(queryMap)) {
            BooleanQuery.Builder builder = new BooleanQuery.Builder();
            queryMap.forEach((key, value) -> {
                if (value instanceof String) {
                    Query queryString = new PhraseQuery(key, (String) value);
//                    Query queryString = new TermQuery(new Term(key, (String) value));
                    builder.add(queryString, BooleanClause.Occur.MUST);
                }
            });
            return searcher.search(builder.build(), 10);
        }
        return null;
    }

}

在 demo.java 中增加搜索代码如下:

//查询数据
   Map<String, Object> map = new HashMap<>();
   map.put("title", "Java 天下第一");
//   map.put("title", "天下第一");
//   map.put("content", "最");
   LuceneSearchUtil searchUtil = LuceneSearchUtil.getInstance();
   TopDocs topDocs = searchUtil.searchByMap(map);
   System.out.println(topDocs.totalHits);

运行结果如下,表示搜索到了两条。

通过可视化工具我们可以看到 title 为"Java 天下第一"确实是有两条记录,而且我们也确认只插入了两条数据。注意这里如果根据其他字符去查询可能查询不出来,因为小编这里的分词器采用的是默认的分词器,小伙伴可以根据自身的情况采用相应的分词器。

至此我们可以索引和搜索数据了,不过这还是简单的入门操作,对于不同类型的字段,我们需要使用不同的查询方式,而且根据系统的特性我们需要使用特定的分词器,默认的标准分词器不一定符合我们的使用场景。而且我们索引数据的时候也需要根据字段类型进行不同 Field 的设定。上面的案例只是 demo 并不能在生产上使用,搜索引擎在互联网行业是领头羊,很多先进的互联网技术都是从搜索引擎开始发展的。

Vue3反编译 在 Vue 3 中,如果你需要反编译已打包或混淆的代码(例如 .vue 文件被编译后的 JavaScript 文件),通常涉及以下场景和工具链。请注意,反编译他人代码可能涉及法律风险,请确保你的操作合法合规(例如仅处理自己拥有版权的代码) 阅读详情

相关推荐

vue 查看dist文件里的结构(多种方式)

本文通过三种方式给大家介绍了vue 查看dist文件里的结构,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下

vue打包后反编译到源代码(reverse-sourcemap)

1.打开cmd,使用管理员打开 2.找到需要编译的js的目录下,这里以d://dist/static/js目录为例 执行: cd d://dist/static/js 执行完成后在该目录会看到目录下存在下面的文件名:app.xxxxxx.js.map,这里以app.5ef4326406dfb69eb193.js.map为例,如下图 图1 3.执行npm,加载reverse-sourcemap资源 执行:npm install --global reverse-sourcemap 4.反

u013015301的博客 2万+

激光可燃气体传感器:核心技术环节分析

激光可燃气体传感器核心技术涵盖七个维度:1)TDLAS光谱吸收与调制技术(检测下限ppm级);2)DFB激光器与双闭环温控系统(波长稳定性±0.0036nm);3)微弱信号处理技术(信噪比提升60dB);4)环境补偿算法(温压补偿误差<±1%);5)本质安全防爆设计(ExiaIICT4Ga等级);6)智能边缘计算(STM32L476芯片);7)多重性能验证(响应时间<1秒)。通过微型化、多气体检测等创新,该技术已在工业安全等领域替代传统方案,实现高精度检测。(149字)

上市公司前技术负责人,20 年软硬件深耕 + 百人研发团队管理经验,名校理工科背景加持。从技术攻坚到团队掌舵,沉淀海量项目实战与管理心得,专注分享软硬件干货、疑难问题解决方案,既是靠谱知识传播者,也盼链接优质项目共成长。 1047

根据vue打包出来的dist文件进行反编译,推出代码

111

weixin_37601321的博客 1万+

Discuz IIS上传附件大于28M失败报错Upload Failed.修改maxAllowedContentLength(图文教程)

下图:Discuz X3.5的系统信息,上传许可为1024MB(1GB)论坛为局域网论坛,仅供内部同事交流使用!使用官方最新的Discuz! X3.5 Release 20231221 UTF-8 下图:选择上传附件(提示可以最大上传100M) 下图:模拟上传84M的文件(

骚团长的博客 1830

DIY压电陶瓷传感器信号放大:从拆解到JFET电路实战(附示波器实测数据)

本文详细介绍了如何利用废旧设备拆解的压电陶瓷片,结合JFET(结型场效应管)高输入阻抗特性,DIY搭建高灵敏度振动传感器放大电路。从元件拆解、参数实测到JFET共源极放大器实战搭建与调试,提供了完整的步骤、示波器实测数据及优化方案,帮助电子爱好者实现低功耗振动检测应用。

weixin_29195615的博客 391

几百行代码完成百度搜索引擎,真的可以吗?

Hello 大家好,我是鸭血粉丝,大家都叫我阿粉,搜索引擎想必大家一定不会默认,我们项目中经常使用的 ElasticSearch 就是一种搜索引擎,在我们的日志系统中必不可少,ELK 作为一个整体,基本上是运维标配了,另外目前的搜索引擎底层都是基于 Lucene 来实现的。 阿粉最近遇到一个需求,因为数据量没有达到需要使用 ElasticSearch 的级别,也不想单独部署一套集群,所以准备自己基于 Lucene 实现一个简易的搜索服务。下面我们一起来看一下吧。 背景 **Lucene **是一套用

AI乔治 429

pytorch gpu运行安装重点(CUDA12.3)

pytorch gpu加速

weixin_45597212的博客 3万+

vue代码可以反编译吗_几百行代码完成百度搜索引擎,真的可以吗?

作者:鸭血粉丝来源:Java极客技术搜索引擎想必大家一定不会默认,我们项目中经常使用的 ElasticSearch 就是一种搜索引擎,在我们的日志系统中必不可少,ELK 作为一个整体,基本上是运维标配了,另外目前的搜索引擎底层都是基于 Lucene 来实现的。最近遇到一个需求,因为数据量没有达到需要使用 ElasticSearch 的级别,也不想单独部署一套集群,所以准备自己基于 Luc...

weixin_39609407的博客 1318

ubuntu18.04安装Nvidia显卡驱动后黑屏及网络、蓝牙驱动消失的解决方案

ubuntu18.04安装Nvidia显卡驱动后黑屏及网络、蓝牙驱动消失的解决方案

weixin_40735291的博客 6619

DDR的Controller、Channel、Chip、Rank、Bank、Row、Column、Sided、Dimm

DDR的Controller、Channel、Chip、Bank、Rank、Row、Column、Sided、Dimm

张飞online博客 2万+

只用了几百行代码写的百度搜索引擎,都是精髓!

一、阿里 (会员中台) 1. Java 重写hashcode()是否需要重写equals(),不重写会有什么后果 2. 并发 自旋锁和阻塞锁的区别 公平锁和非公平锁的区别 jdk中哪种数据结构或工具可以实现当多个线程到达某个状态时执行一段代码 栅栏和闭锁的区别 如何使用信号量实现上述情况 3. JVM 新生代和年老代的GC算法分别是什么 标记清除和标记整理的区别 了解过CMS收集器吗 4. 网络 解释HTTPs HTTPs为什么要用对称加密+非对称加密,相对于只使用非对称加密有什么好处 5.

m0_57983911的博客 1294

OKCC呼叫中心部署实战指南:从零搭建高效外呼系统

本文提供OKCC呼叫中心系统的完整部署实战指南,从服务器选型、操作系统准备到详细安装步骤和性能优化,帮助企业从零搭建高效外呼系统。重点介绍FreeSWITCH核心组件的安装配置技巧,以及常见问题排查方法,助力企业提升客户服务效率。

weixin_29275257的博客 164

只用了几百行代码写的百度搜索引擎,你看咋样?

writer.addDocument(document); writer.commit(); writer.close(); } /** 索引实体列表 @param objects 实例列表 @throws IOException IO 异常 */ public void addModelDocs(List<?> objects) throws IOException { if (CollectionUtils.isNotEmpty(objects)) { List docs = new A

m0_70658519的博客 435

【Java核心高阶进阶】44-Java8核心新特性

模块十:Java新特性演进 · 第 1 篇Java 8 是 Java 语言历史上最重要的一次发布,它引入的函数式编程范式从根本上改变了 Java 的编码方式。即便到了 JDK 21 的今天,Lambda、Stream、Optional 仍是日常开发的高频工具。本篇将深入这些特性的语法、原理与底层机制,帮助你不仅会用,更知其所以然。

gaochufukanyongren的博客 24

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

只用了几百行代码写的百度搜索引擎

前言 不知道大家有没有这样的经历,明明感觉自己这个技术掌握的还行,能写在简历里肯定是有点自信的,但是被面试官一问,就感觉啥也不是 当我从面试的大楼里出来的时候脑袋里还是昏昏沉沉的,你压根不知道我经历了什么,我就面试一个开发岗位,有必要问我这么多 后来咨询了一下我在阿里的师兄,他是这么说的:现在的就业竞争很激烈,在考察技术的时候不单单是停留在应用层面,底层的原理你也要有所了解,这样在遇到不可控问题的时候你才能游刃有余的解决掉。 跟他谈了蛮久,了解到我一些问题后,直接给了我一份他的学习笔记,让我回去用两个星期啃

m0_57624876的博客 199

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

【面试必会】只用了几百行代码写的百度搜索引擎

Cache aside Cache aside也就是旁路缓存,是比较常用的缓存策略。 (1)读请求常见流程 应用首先会判断缓存是否有该数据,缓存命中直接返回数据,缓存未命中即缓存穿透到数据库,从数据库查询数据然后回写到缓存中,最后返回数据给客户端。 (2)写请求常见流程 首先更新数据库,然后从缓存中删除该数据。 看了写请求的图之后,有些同学可能要问了:为什么要删除缓存,直接更新不就行了?这里涉及到几个坑,我们一步一步踩下去。 Cache aside踩坑 Cache aside策略如果用错就会遇到深坑,下

m0_57286743的博客 178

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Elasticsearch-1.es核心概念

Elasticsearch 小知识环节:select * from product where brand like ‘%小米NFC手机%’,前面这段brand 就算加了索引,在使用 like % 的时候也会造成全表扫描

飘然渡沧海的博客 951

只用了几百行代码写的百度搜索引擎,你看咋样?,web开发难点

javascript是前端必要掌握的真正算得上是编程语言的语言,学会灵活运用javascript,将对以后学习工作有非常大的帮助。掌握它最重要的首先是学习好基础知识,而后通过不断的实战来提升我们的编程技巧和逻辑思维。这一块学习是持续的,直到我们真正掌握它并且能够灵活运用它。如果最开始学习一两遍之后,发现暂时没有提升的空间,我们可以暂时放一放。继续下面的学习,javascript贯穿我们前端工作中,在之后的学习实现里也会遇到和锻炼到。真正学习起来并不难理解,关键是灵活运用。

2401_84097964的博客 383

互联网寒冬公司倒闭后,只用了几百行代码写的百度搜索引擎

分享一些系统的面试题,大家可以拿去刷一刷,准备面试涨薪。

2401_83704218的博客 1140
上一篇: 我凭借这个压缩包拿下了蚂蚁金服、字节跳动、小米等大厂的offer
下一篇: Alibaba内部 高并发/性能优化 实操手册曝光!网友:只有阿里能有这水平
「已注销」
博客等级 码龄6年 36粉丝 149原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值