ElasticSearch基本使用

ElasticSearch基本使用

说明:基于 elasticsearch/elasticsearch 扩展包的 php 版使用。
文档:https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/getting-started-php.html

一、自定义服务类

<?php
/**
 * Created by PhpStorm
 * User: Jason
 * Date: 2024-06-07
 * Time: 14:48
 */

require_once '../vendor/autoload.php';

use Elasticsearch\ClientBuilder;

class ES
{
    /**
     * 客户端
     *
     * @var \Elasticsearch\Client
     */
    private $client;

    /**
     * @param array $hosts
     */
    public function __construct(array $hosts = ['127.0.0.1'])
    {
        $this->client = ClientBuilder::create()->setHosts($hosts)->build();
    }

    // ================================= 索引相关操作 =================================================

    /**
     * 创建索引
     * 说明:相当于Mysql的表,只需要创建一次;原本相当于database,8.x废除了type(相当于table)
     *
     * @param string $index 索引名称
     * @param array $settings 索引设置
     * @param array $mappings 索引映射
     * @return array
     */
    public function creatIndex(string $index, array $settings = [], array $mappings = []): array
    {
        $params = [
            'index' => $index,
            'body' => [
                'settings' => $settings,
                'mappings' => $mappings,
            ]
        ];

        return $this->client->indices()->create($params);
    }

    /**
     * 删除索引
     *
     * @param $indexName string 索引名称
     * @return array
     */
    public function deleteIndex(string $indexName): array
    {
        $params = [
            'index' => $indexName,
        ];

        return $this->client->indices()->delete($params);
    }

    /**
     * 检查索引是否存在
     *
     * @param string $indexName
     * @return bool
     */
    public function existsIndex(string $indexName)
    {
        return $this->client->indices()->exists(['index' => $indexName]);
    }

    /**
     * 获取索引设置
     *
     * @param string $indexName 索引名称
     * @return array
     */
    public function getIndexSettings(string $indexName): array
    {
        $params = [
            'index' => $indexName
        ];

        return $this->client->indices()->getSettings($params);
    }

    /**
     * 关闭索引
     *
     * @param $indexName string 索引名称
     * @return array
     */
    public function closeIndex(string $indexName): array
    {
        $params = ['index' => $indexName];

        return $this->client->indices()->close($params);
    }

    /**
     * 打开索引
     *
     * @param $indexName string 索引名称
     * @return array
     */
    public function openIndex(string $indexName): array
    {
        $params = ['index' => $indexName];

        return $this->client->indices()->open($params);
    }

    /**
     * 更新索引设置
     *
     * @param string $indexName 索引名称
     * @param array $settings 新的索引设置
     * @return array 更新索引设置的响应
     */
    public function updateIndexSettings(string $indexName, array $settings): array
    {
        $params = [
            'index' => $indexName,
            'body' => ['settings' => $settings]
        ];

        return $this->client->indices()->putSettings($params);
    }

    /**
     * 更新非动态索引设置
     *
     * @param $indexName string 索引名称
     * @param array $settings 索引设置
     * @return array
     */
    public function updateNonDynamicIndexSettings(string $indexName, array $settings)
    {
        // 关闭索引
        $this->closeIndex($indexName);

        // 更新索引设置
        $response = $this->updateIndexSettings($indexName, $settings);

        // 重新打开索引
        $this->openIndex($indexName);

        return $response;
    }

    /**
     * 获取索引映射
     *
     * @param string $indexName 索引名称
     * @return array 获取索引映射的响应
     */
    public function getIndexMappings($indexName)
    {
        $params = [
            'index' => $indexName
        ];

        return $this->client->indices()->getMapping($params);
    }

    /**
     * 更新索引映射
     *
     * @param string $indexName 索引名称
     * @param array $mappings 新的索引映射
     * @return array 更新索引映射的响应
     */
    public function updateIndexMappings(string $indexName, array $mappings): array
    {
        $params = [
            'index' => $indexName,
            'body' => ['properties' => $mappings]
        ];

        return $this->client->indices()->putMapping($params);
    }

    /**
     * 刷新索引
     *
     * @param $indexName string 索引名称
     * @return array
     */
    public function refreshIndex(string $indexName): array
    {
        $params = [
            'index' => $indexName
        ];

        return $this->client->indices()->refresh($params);
    }

    // ===================================== 文档相关操作 ===============================================

    /**
     * 新增文档
     *
     * @param string $indexName 索引名称
     * @param string $id 文档ID
     * @param array $body 文档内容
     * @return array
     */
    public function addDocument($indexName, string $id, array $body)
    {
        $params = [
            'index' => $indexName,
            'id' => $id, // 文档ID,可省略,默认生成随机ID
            'body' => $body
        ];

        return $this->client->index($params);
    }

    /**
     * 批量新增文档
     *
     * @param array $params
     * @return array|callable
     */
    public function batchAddDocument(array $params)
    {
        return $this->client->bulk($params);
    }

    /**
     * 获取文档
     * 说明:查询单挑记录
     *
     * @param string $indexName 索引名称
     * @param string $id 文档ID
     * @return array|callable
     */
    public function getDocument(string $indexName, string $id): callable|array
    {
        $params = [
            'index' => $indexName,
            'id' => $id,
        ];

        return $this->client->get($params);
    }

    /**
     * 更新文档
     *
     * @param string $indexName 索引名称
     * @param string $id 文档ID
     * @param array $doc 更新内容
     * @return array|callable
     */
    public function updateDocument(string $indexName, string $id, array $doc)
    {
        $params = [
            'index' => $indexName,
            'id' => $id,
            'body' => [
                'doc' => $doc, // 需要更新的内容
            ],
        ];

        return $this->client->update($params);
    }

    /**
     * 删除文档
     *
     * @param string $indexName
     * @param $id
     * @return array|callable
     */
    public function deleteDocument(string $indexName, $id)
    {
        $params = [
            'index' => $indexName,
            'id' => $id,
        ];

        return $this->client->delete($params);
    }

    // ======================================= 搜索相关操作 ================================================

    /**
     * 基础搜索方法
     *
     * @param string $indexName 索引名称
     * @param array $query 搜索查询
     * @param array|null $sort 排序参数
     * @param int|null $from 起始位置
     * @param int|null $size 返回结果数
     * @return array
     */
    public function search(string $indexName, array $query, array $sort = null, int $from = null, int $size = null)
    {
        $params = [
            'index' => $indexName,
            'body' => [
                'query' => $query
            ]
        ];

        if ($sort) {
            $params['body']['sort'] = $sort;
        }

        if (!is_null($from)) {
            $params['body']['from'] = $from;
        }

        if (!is_null($size)) {
            $params['body']['size'] = $size;
        }

        return $this->client->search($params);
    }
}

二、索引基本使用

2.1 创建索引

<?php
$hosts = [
    '192.168.0.117:9200'
];

$es = new ES($hosts);

// 索引名
$userIndex = 'user';

// 设置
$settings = [
    // 分片数量: 一个索引库将拆分成多片分别存储不同的结点,默认5个
    'number_of_shards' => count($hosts),
    // 每个分片分配的副本数,replica shard是primary shard的副本,负责容错,以及承担读请求负载,如果服务器只有一台,只能设置为0
    'number_of_replicas' => 0
];

// 创建文档映射,就是文档存储在ES中的数据结构
$mappings = [
    'properties' => [
        'user_id' => [
            'type' => 'integer',
            'index' => 'true'
        ],
        'nickname' => [
            // 数据类型: text支持分词; keyword不支持分词,只能精确索引;数值类型(integer,...),日期类型(date),逻辑类型:boolean;IP类型(ip);地理坐标类型(geo_point);.....
            // keyword 排序是按照字符串的ASCII码排序的,'3'>'20'
            'type' => 'text',
            // 字段可以被索引,也就是能用来当做查询条件来查询,只能填写true和false
            'index' => 'true',
            // 索引分词器,用于字符串类型,这里使用中文分词器ik,用默认分词器可以省略
            'analyzer' => 'ik_max_word',
            // 搜索分词器,用于搜索关键词的分词器
            'search_analyzer' => 'ik_max_word',
        ],
        'age' => [
            'type' => 'integer',
            'index' => 'true',
        ],
        'create_time' => [
            'type' => 'date',
            'index' => 'true',
            'format' => 'yyyy-MM-dd HH:mm:ss'
        ],
    ]
];

$res = $es->creatIndex($userIndex, $settings, $mappings);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"acknowledged":true,"shards_acknowledged":true,"index":"user"}

2.2 判断索引是否存在

<?php
$res = $es->existsIndex($userIndex);
var_dump($res); 
// 输出: true

2.3 删除索引

<?php
$res = $es->deleteIndex($userIndex);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"acknowledged":true}

2.4 获取索引设置

<?php
$res = $es->getIndexSettings($userIndex);
var_dump(json_encode($res,JSON_UNESCAPED_UNICODE)); 
// 输出: {"user":{"settings":{"index":{"routing":{"allocation":{"include":{"_tier_preference":"data_content"}}},"refresh_interval":"1s","number_of_shards":"1","provided_name":"user","creation_date":"1719907928617","number_of_replicas":"0","uuid":"7d3Mojg6SL6q_7Q0gamMog","version":{"created":"8503000"}}}}}

2.5 更新索引设置

<?php
$settings = [
    'index.refresh_interval' => '1s' // 刷新时间间隔
];
$res = $es->updateNonDynamicIndexSettings($userIndex, $settings);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"acknowledged":true}

2.6 获取索引映射

<?php
$res = $es->getIndexMappings($userIndex);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"user":{"mappings":{"properties":{"age":{"type":"integer"},"create_time":{"type":"date","format":"yyyy-MM-dd HH:mm:ss"},"nickname":{"type":"text","analyzer":"ik_max_word"},"user_id":{"type":"integer"}}}}}

2.7 更新索引映射

<?php
$mappings = [
    'phone' => [
        'type' => 'keyword',
        'index' => 'true'
    ]
];
$res = $es->updateIndexMappings($userIndex, $mappings);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"acknowledged":true}

2.8 刷新索引

<?php
$res = $es->refreshIndex($userIndex);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出:  {"_shards":{"total":1,"successful":1,"failed":0}}

三、文档基本使用

3.1 新增文档

<?php
$res = $es->addDocument($userIndex, '1', [
    'user_id' => 1,
    'nickname' => '爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)',
    'age' => mt_rand(10, 40),
    'create_time' => \Carbon\Carbon::now()->toDateTimeString(),
    'phone' => '13612348888'
]);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"_index":"user","_id":"1","_version":1,"result":"created","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":0,"_primary_term":5}

3.2 批量新增文档

<?php
$documents = [
    [
        'user_id' => 2,
        'nickname' => '高颜值情侣玻璃浮雕吸管杯2个-xh',
        'age' => 12,
        'create_time' => '2024-06-29 12:00:05',
        'phone' => '13575861234',
    ],
    [
        'user_id' => 3,
        'nickname' => '网红爆款护手霜6支香味随机-K',
        'age' => 23,
        'create_time' => '2024-06-28 03:18:08',
        'phone' => '19512345286',
    ],
    [
        'user_id' => 4,
        'nickname' => '【可拆卸一拖四线】充电宝迷你自带线大容量数显快充移动电源',
        'age' => 34,
        'create_time' => '2024-07-02 03:18:08',
        'phone' => '19538883347',
    ],
];

$params = [];
foreach ($documents as $document) {
    $params['body'][] = [
        'index' => [
            '_index' => $userIndex,
            '_id' => strval($document['user_id'])
        ]
    ];

    $params['body'][] = [
        'user_id' => $document['user_id'],
        'nickname' => $document['nickname'],
        'age' => $document['age'],
        'create_time' => $document['create_time'],
        'phone' => $document['phone'],
    ];
}
var_dump(json_encode($es->batchAddDocument($params), JSON_UNESCAPED_UNICODE)); 
// 输出: {"errors":false,"took":15,"items":[{"index":{"_index":"user","_id":"2","_version":1,"result":"created","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":3,"_primary_term":5,"status":201}},{"index":{"_index":"user","_id":"3","_version":1,"result":"created","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":4,"_primary_term":5,"status":201}},{"index":{"_index":"user","_id":"4","_version":1,"result":"created","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":5,"_primary_term":5,"status":201}}]}

3.3 获取文档

<?php
var_dump(json_encode($es->getDocument($userIndex, '9'), JSON_UNESCAPED_UNICODE)); 
// 输出: {"_index":"user","_id":"1","_version":3,"_seq_no":2,"_primary_term":5,"found":true,"_source":{"user_id":1,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":29,"create_time":"2024-07-02 17: 15: 16","phone":"13612348888"}}

3.4 更新文档

<?php
$res = $es->updateDocument($userIndex, '1', [
    'nickname' => '9999Jason的爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)',
    'age' => 30
]);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"_index":"user","_id":"1","_version":2,"result":"updated","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":7,"_primary_term":5}

3.5 删除文档

<?php
$res = $es->deleteDocument($userIndex, '9');
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE));
// 输出: {"_index":"user","_id":"9","_version":3,"result":"deleted","_shards":{"total":1,"successful":1,"failed":0},"_seq_no":8,"_primary_term":5}

四、文档搜索

4.1 词条匹配查询(精确匹配查询): 类似 where age = xx

<?php
$query = [
    'term' => [
        'age' => 34
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":null,"hits":[{"_index":"user","_id":"4","_score":null,"_source":{"user_id":4,"nickname":"【可拆卸一拖四线】充电宝迷你自带线大容量数显快充移动电源","age":34,"create_time":"2024-07-02 03: 18: 08","phone":"19538883347"},"sort":[1719890288000,4]}]}}

4.2 多条件查询 : 类似 where xxx and xxx

<?php
$query = [
    'bool' => [
        'must' => [
            [
                'term' => [
                    'age' => 29
                ],
            ],
            [
                'term' => [
                    'phone' => '13612348888'
                ],
            ]
        ]
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); // 输出: {"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":2.3862944,"hits":[{"_index":"user","_id":"1","_score":2.3862944,"_source":{"user_id":1,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":29,"create_time":"2024-07-02 17:15:16","phone":"13612348888"}}]}}

4.3 通配符查询

说明:对于keyword等不支持分词的类型,模糊查询使用通配符(wildcard)查询;

<?php
$query = [
    'wildcard' => [
        "nickname" => '*肉松*' // 使用 * 作为通配符,* 可以匹配任意字符序列(包括空字符序列)。例如,*elastic* 可以匹配 elastic 、my elastic search 、elasticity 等
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":1,"hits":[{"_index":"user","_id":"1","_score":1,"_source":{"user_id":1,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":29,"create_time":"2024-07-02 17: 15: 16","phone":"13612348888"}},{"_index":"user","_id":"5","_score":1,"_source":{"user_id":5,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":34,"create_time":"2024-07-03 09: 28: 29","phone":"19575621495"}}]}}

4.4 模糊查询

说明:对于text等支持分词的类型,推荐使用fuzzy进行模糊查询

<?php
$query = [
    'fuzzy' => [
        "nickname" => [
            'value' => '肉松',
            'fuzziness' => 1 // fuzziness 参数指定了允许的模糊程度。值越大,允许的差异越大。例如,fuzziness: 2 可能允许一些字符的替换、插入或删除
        ]
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":0.8915597,"hits":[{"_index":"user","_id":"1","_score":0.8915597,"_source":{"user_id":1,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":29,"create_time":"2024-07-02 17: 15: 16","phone":"13612348888"}},{"_index":"user","_id":"5","_score":0.8915597,"_source":{"user_id":5,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":34,"create_time":"2024-07-03 09: 28: 29","phone":"19575621495"}}]}}

4.5 前缀查询

说明: 前缀查询通常适用于keyword类型的字段,这种类型的字段不会进行分词处理,会将整个值作为一个词条来建立索引

<?php
$query = [
    'prefix' => [
        "phone" => '195'
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":3,"relation":"eq"},"max_score":1,"hits":[{"_index":"user","_id":"3","_score":1,"_source":{"user_id":3,"nickname":"网红爆款护手霜6支香味随机-K","age":23,"create_time":"2024-06-28 03: 18: 08","phone":"19512345286"}},{"_index":"user","_id":"4","_score":1,"_source":{"user_id":4,"nickname":"【可拆卸一拖四线】充电宝迷你自带线大容量数显快充移动电源","age":34,"create_time":"2024-07-02 03: 18: 08","phone":"19538883347"}},{"_index":"user","_id":"5","_score":1,"_source":{"user_id":5,"nickname":"爆款煎饼(传统双蛋煎饼+肉松+优质火腿片+配菜+薄脆)","age":34,"create_time":"2024-07-03 09: 28: 29","phone":"19575621495"}}]}}

4.6 联合查询 - and

SQL: where age in (23,34) and create_time between 2024-07-01 00:00:00 and 2024-07-02 23:59:59

<?php
$query = [
    'bool' => [
        // must(且): 数组里面的条件都要满足,该条数据才会被选择
        'must' => [
            [
                'terms' => [
                    'age' => [23, 34] // 类似于 where in
                ]
            ],
            [
                'range' => [ // 类似 between
                    'create_time' => [
                        'gte' => '2024-07-01 00:00:00',
                        'lte' => '2024-07-02 23:59:59',
                    ]
                ]
            ]
        ]
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE)); 
// 输出: {"took":3,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":2,"hits":[{"_index":"user","_id":"4","_score":2,"_source":{"user_id":4,"nickname":"【可拆卸一拖四线】充电宝迷你自带线大容量数显快充移动电源","age":34,"create_time":"2024-07-02 03: 18: 08","phone":"19538883347"}}]}}

4.7 联合查询 - or

SQL: where (age <=18 or age >=34) and between 2024-07-01 00:00:00 and 2024-07-02 23:59:59

<?php
$query = [
    'bool' => [
        "must" => [
            [
                'range' => [
                    'create_time' => [
                        'gte' => '2024-06-29 00:00:00',
                        'lte' => '2024-07-02 04:00:00',
                    ]
                ]
            ]
        ],
        // should(或): 数组里面的条件满足其中一个,该条数据被选择
        "should" => [
            [
                'range' => [
                    'age' => [
                        'lte' => 18,
                    ]
                ]
            ],
            [
                'range' => [
                    'age' => [
                        'gte' => 34,
                    ]
                ]
            ]
        ]
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE));
 // 输出: {"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":2,"hits":[{"_index":"user","_id":"2","_score":2,"_source":{"user_id":2,"nickname":"高颜值情侣玻璃浮雕吸管杯2个-xh","age":12,"create_time":"2024-06-29 12: 00: 05","phone":"13575861234"}},{"_index":"user","_id":"4","_score":2,"_source":{"user_id":4,"nickname":"【可拆卸一拖四线】充电宝迷你自带线大容量数显快充移动电源","age":34,"create_time":"2024-07-02 03: 18: 08","phone":"19538883347"}}]}}

4.8 联合查询 - or and or

SQL: where (age >= 34 or age <= 18) and (create_time >= ‘2024-07-02 00:00:00’ or create_time <= ‘2024-06-30 00:00:00’)

<?php
# 联合查询
$query = [
    'bool' => [
        'must' => [
            [
                'bool' => [
                    'should' => [
                        [
                            'range' => [
                                'age' => [
                                    'lte' => 18,
                                ]
                            ]
                        ],
                        [
                            'range' => [
                                'age' => [
                                    'gte' => 34,
                                ]
                            ]
                        ]
                    ]
                ]
            ],
            [
                'bool' => [
                    'should' => [
                        [
                            'range' => [
                                'create_time' => [
                                    'lte' => '2024-06-30 00:00:00',
                                ]
                            ]
                        ],
                        [
                            'range' => [
                                'create_time' => [
                                    'gte' => '2024-07-02 00:00:00',
                                ]
                            ]
                        ]
                    ]
                ]
            ]
        ]
    ]
];
$res = $es->search($userIndex, $query);
var_dump(json_encode($res, JSON_UNESCAPED_UNICODE));
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JasonHome

你的鼓励是我创作的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值