一步步使用SpringBoot结合Vue实现登录和用户管理功能

<el-button

type=“primary”

style=“width: 100%; border: none”

@click=“login”

登录</el-button

2.7、HelloWorld.vue

大家应该还记得,到目前为止,我们 的 / 路径还是指向 HelloWorld.vue 这个组件,为了演示 vuex 状态的全局使用,我们做一些更改,添加一个生命周期的钩子函数,来获取 store中存储的用户名:

computed: {

userName() {

return this.$store.state.user.userName

}

}

完整的 HelloWorld.vue

{{userName}}

我们看一下修改之后的整体效果:

访问首页会自动跳转到登录页,登录成功之后,会记录登录状态。

F12 打开谷歌开发者工具:

  • 打开 Application ,在 Session Storage 中看到我们存储的信息

image-20210123000856893

  • 打开vue 开发工具,在 Vuex 中也能看到我们 store中的数据

image-20210123001144379

  • 再次登录,打开Network,可以发现异步式请求请求头里已经添加了 token

image-20210123103330123

再次说一下,这里偷了懒,登录用封装的公共请求方法是不合理的,毕竟登录就是为了获取token,request.js又对token进行了拦截,所以我怼我自己😂 比较好的做法可以参考 vue-element-admin ,在 store 中写 action 用来登录。

五、用户管理功能

====================================================================

上面我们已经写了一个简单的登录功能,通过这个功能,基本可以对SpringBoot+Vue前后端分离开发有有一个初步了解,在实际工作中,一般的工作都是基于基本框架已经成型的项目,登录、鉴权、动态路由、请求封装这些基础功能可能都已经成型。所以后端的日常工作就是写接口写业务 ,前端的日常工作就是 调接口写界面,通过接下来的用户管理功能,我们能熟悉这些日常的开发。

1、后端开发


后端开发,crud就完了。

1.1、自定义分页查询

按照官方文档,来进行MP的分页。

1.1.1、分页配置

首先需要对分页进行配置,创建分页配置类

/**

  • @Author 三分恶

  • @Date 2021/1/23

  • @Description MP分页设置

*/

@Configuration

@MapperScan(“cn.fighter3.mapper..mapper”)

public class MybatisPlusConfig {

@Bean

public PaginationInterceptor paginationInterceptor() {

PaginationInterceptor paginationInterceptor = new PaginationInterceptor();

// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false

// paginationInterceptor.setOverflow(false);

// 设置最大单页限制数量,默认 500 条,-1 不受限制

// paginationInterceptor.setLimit(500);

// 开启 count 的 join 优化,只针对部分 left join

paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));

return paginationInterceptor;

}

}

1.1.2、自定义sql

作为Mybatis的增强工具,MP自然是支持自定义sql的。其实在MP中,单表操作基本上是不用自己写sql。这里只是为了演示MP的自定义sql,毕竟在实际应用中,批量操作、多表操作还是更适合自定义sql实现。

  • 修改pom.xml,在 中添加:

src/main/java

**/*.xml

true

src/main/resources

  • 配置文件:在application.properties中添加mapper扫描路径及实体类别名包

mybatis-plus

mybatis-plus.mapper-locations=classpath:cn/fighter3/mapper/*.xml

mybatis-plus.type-aliases-package=cn.fighter3.entity

  • 在UserMapper.java 中定义分页查询的方法

IPage selectUserPage(Page page,String keyword);

  • 在UserMapper.java 同级目录下新建 UserMapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>

select * from user

or login_name like CONCAT(‘%’,#{keyword},‘%’)

or user_name like CONCAT(‘%’,#{keyword},‘%’)

or email like CONCAT(‘%’,#{keyword},‘%’)

or address like CONCAT(‘%’,#{keyword},‘%’)

这个查询也比较简单,根据关键字查询用户。

OK,我们的自定义分页查询就完成了,可以写个单元测试测一下。

1.2、控制层

新建UserControler,里面也没什么东西,增删改查的接口:

/**

  • @Author 三分恶

  • @Date 2021/1/23

  • @Description 用户管理

*/

@RestController

public class UserController {

@Autowired

private UserService userService;

/**

  • 分页查询

  • @param queryDTO

  • @return

*/

@PostMapping(“/api/user/list”)

public Result userList(@RequestBody QueryDTO queryDTO){

return new Result(200,“”,userService.selectUserPage(queryDTO));

}

/**

  • 添加

  • @param user

  • @return

*/

@PostMapping(“/api/user/add”)

public Result addUser(@RequestBody User user){

return new Result(200,“”,userService.addUser(user));

}

/**

  • 更新

  • @param user

  • @return

*/

@PostMapping(“/api/user/update”)

public Result updateUser(@RequestBody User user){

return new Result(200,“”,userService.updateUser(user));

}

/**

  • 删除

  • @param id

  • @return

*/

@PostMapping(“/api/user/delete”)

public Result deleteUser(Integer id){

return new Result(200,“”,userService.deleteUser(id));

}

/**

  • 批量删除

  • @param ids

  • @return

*/

@PostMapping(“/api/user/delete/batch”)

public Result batchDeleteUser(@RequestBody List ids){

userService.batchDelete(ids);

return new Result(200,“”,“”);

}

}

这里写的也比较简单,直接调用服务层的方法。

1.3、服务层

接口这里就不再贴出了,实现类如下:

/**

  • @Author 三分恶

  • @Date 2021/1/23

  • @Description

*/

@Service

public class UserServiceImpl implements UserService {

@Autowired

private UserMapper userMapper;

/**

  • 分页查询

**/

@Override

public IPage selectUserPage(QueryDTO queryDTO) {

Page page=new Page<>(queryDTO.getPageNo(),queryDTO.getPageSize());

return userMapper.selectUserPage(page,queryDTO.getKeyword());

}

@Override

public Integer addUser(User user) {

return userMapper.insert(user);

}

@Override

public Integer updateUser(User user) {

return userMapper.updateById(user);

}

@Override

public Integer deleteUser(Integer id) {

return userMapper.deleteById(id);

}

@Override

public void batchDelete(List ids) {

userMapper.deleteBatchIds(ids);

}

}

这里也比较简单,也没什么业务逻辑。

实际上,业务层至少也会做一些参数校验的工作——我见过有的系统,只是在客户端进行了参数校验,实际上,服务端参数校验是必需的(如果不做,会被怼😔),因为客户端校验相比较服务端校验是不可靠的。

在分页查询 public IPage<User> selectUserPage(QueryDTO queryDTO) 里用了一个业务对象,这种写法,也可以用一些参数校验的插件。

1.4、业务实体

上面用到了一个业务实体对象,创建一个 业务实体类QueryDTO ,定义了一些参数,这个类主要用于前端向后端传输数据,可以可以使用一些参数校验插件添加参数校验规则。

/**

  • @Author 三分恶

  • @Date 2021/1/23

  • @Description 查询业务实体

  • 这里仅仅定义了三个参数,在实际应用中可以定义多个参数

*/

public class QueryDTO {

private Integer pageNo; //页码

private Integer pageSize; //页面大小

private String keyword; //关键字

//省略getter、setter

}

简单测一下,后端👌

image-20210126172536248

2、前端开发


2.1、首页

在前面,登录之后,跳转到HelloWorld,还是比较简陋的。本来想直接跳到用户管理的视图,觉得不太好看,所以还是写了一个首页,当然这一部分不是重点。

见过一些后台管理系统的都知道,后台管理系统大概都是像下面的布局:

后台布局

在ElementUI中提供了这样的布局组件Container 布局容器:

image-20210126173415562

大家都知道根组件是 App.vue ,当然在App.vue中写整体布局是不合适的,因为还有登录页面,所以在 views 下新建 home.vue,采用Container 布局容器来进行布局,使用NavMenu 导航菜单来创建侧边栏。

当然,比较好的做法是home.vue里不写什么内容,将顶部和侧边栏都抽出来作为子页面(组件)。

Just A Demo

<el-avatar

icon=“el-icon-user-solid”

style=“color: #222; float: right; padding: 20px”

{{ this.$store.state.user.userName }}</el-avatar

<el-menu

:default-active=“$route.path”

router

text-color=“black”

active-text-color=“red”

<el-menu-item

v-for=“(item, i) in navList”

:key=“i”

:index=“item.name”

{{ item.title }}

注意 <el-main> 用了路由占位符 <router-view></router-view> ,在路由src\router\index.js里进行配置,就可以加载我们的子路由了:

{

path: ‘/’,

name: ‘Default’,

redirect: ‘/home’,

component: Home

},

{

path: ‘/home’,

name: ‘Home’,

component: Home,

meta: {

requireAuth: true

},

redirect: ‘/index’,

children:[

{

path:‘/index’,

name:‘Index’,

component:() => import(‘@/views/home/index’),

meta:{

requireAuth:true

}

},

}

]

},

首页本来不想放什么东西,后来想想,还是放了点大家爱看的——没别的意思,快过年了,各位姐夫过年好。🏮😀

image-20210126174723686

图片来自冰冰微博,见水印。

2.2、用户列表

views下新建 user 目录,在 user 目录下新建 index.vue ,然后添加为home的子路由:

{

path: ‘/home’,

name: ‘Home’,

component: Home,

meta: {

requireAuth: true

},

redirect: ‘/index’,

children:[

{

path:‘/index’,

name:‘Index’,

component:() => import(‘@/views/home/index’),

meta:{

requireAuth:true

}

},

{

path:‘/user’,

name:‘User’,

component:()=>import(‘@/views/user/index’),

meta:{

requireAuth:true

}

}

]

},

接下来开始用户列表功能的编写。

  • 首先封装一下api,在user.js中添加调用分页查询接口的api

//获取用户列表

export function userList(data) {

return request({

url: ‘/user/list’,

method: ‘post’,

data

})

}

  • user/index.vue 中导入userList

import { userList} from “@/api/user”;

  • 为了在界面初始化的时候加载用户列表,使用了生命周期钩子来调用接口获取用户列表,代码直接一锅炖了

export default {

data() {

return {

userList: [], // 用户列表

total: 0, // 用户总数

// 获取用户列表的参数对象

queryInfo: {

keyword: “”, // 查询参数

pageNo: 1, // 当前页码

pageSize: 5, // 每页显示条数

},

}

created() { // 生命周期函数

this.getUserList()

},

methods: {

getUserList() {

userList(this.queryInfo)

.then((res) => {

if (res.data.code === 200) {

//用户列表

this.userList = res.data.data.records;

this.total = res.data.data.total;

} else {

this.$message.error(res.data.message);

}

})

.catch((err) => {

console.log(err);

});

},

}

  • 取到的数据,我们用一个表格组件来进行绑定

<el-table

:data=“userList”

border

stripe

效果如下,点击用户管理:

image-20210126184434700

2.3、分页

在上面的图里,我们看到了在最下面有分页栏,我们接下来看看分页栏的实现。

我们这里使用了 Pagination 分页组件:

image-20210126184833582

<el-pagination

@size-change=“handleSizeChange”

@current-change=“handleCurrentChange”

:current-page=“queryInfo.pageNo”

:page-sizes=“[1, 2, 5, 10]”

:page-size=“queryInfo.pageSize”

layout=“total, sizes, prev, pager, next, jumper”

:total=“total”

两个监听事件:

// 监听 pageSize 改变的事件

handleSizeChange(newSize) {

// console.log(newSize)

this.queryInfo.pageSize = newSize;

// 重新发起请求用户列表

this.getUserList();

},

// 监听 当前页码值 改变的事件

handleCurrentChange(newPage) {

// console.log(newPage)

this.queryInfo.pageNo = newPage;

// 重新发起请求用户列表

this.getUserList();

},

2.4、检索用户

搜索框已经绑定了queryInfo.keyword,只需要给顶部的搜索区域添加按钮点击和清空事件——重新获取用户列表:

<el-input

placeholder=“请输入内容”

v-model=“queryInfo.keyword”

clearable

@clear=“getUserList”

<el-button

slot=“append”

icon=“el-icon-search”

@click=“getUserList”

效果如下:

image-20210126185429397

2.5、添加用户

  • 还是先写api,导入后面就略过了

//添加用户

export function userAdd(data) {

return request({

url: ‘/user/add’,

method: ‘post’,

data

})

}

  • 添加用户我们用到了两个组件 Dialog 对话框组件和 Form 表单组件。

<el-dialog

title=“添加用户”

:visible.sync=“addDialogVisible”

width=“30%”

@close=“addDialogClosed”

<el-button @click=“addDialogVisible = false”>取 消

<el-button type=“primary” @click=“addUser”>确 定

  • 使用 addDialogVisible 控制对话框可见性,使用userForm 绑定修改用户表单:

addDialogVisible: false, // 控制添加用户对话框是否显示

userForm: {

//用户

loginName: “”,

userName: “”,

password: “”,

sex: “”,

email: “”,

address: “”,

},

  • 两个函数,addUser 添加用户,addDialogClosed 在对话框关闭时清空表单

//添加用户

addUser() {

userAdd(this.userForm)

.then((res) => {

if (res.data.code === 200) {

this.addDialogVisible = false;

this.getUserList();

this.$message({

message: “添加用户成功”,

type: “success”,

});

} else {

this.$message.error(“添加用户失败”);

}

})

.catch((err) => {

this.$message.error(“添加用户异常”);

console.log(err);

});

},

// 监听 添加用户对话框的关闭事件

addDialogClosed() {

// 表单内容重置为空

this.$refs.addFormRef.resetFields();

},

效果:

image-20210126190500082

在最后一页可以看到我们添加的用户:

image-20210126190528809

2.6、修改用户

  • 先写api

//修改用户

export function userUpdate(data) {

return request({

url: ‘/user/update’,

method: ‘post’,

data

})

}

  • 在修改用户这里,我们用到一个作用域插槽,通过slot-scope="scope"接收了当前作用域的数据,然后通过scope.row拿到对应这一行的数据,再绑定具体的属性值就行了。

<el-button

type=“primary”

size=“mini”

icon=“el-icon-edit”

@click=“showEditDialog(scope.row)”

  • 具体的修改仍然是用对话框加表单的形式

<el-button @click=“editDialogVisible = false”>取 消

<el-button type=“primary” @click=“editUser”>确 定

  • editDialogVisible控制对话框显示,editForm 绑定修改用户表单

editDialogVisible: false, // 控制修改用户信息对话框是否显示

editForm: {

id: “”,

loginName: “”,

userName: “”,

password: “”,

sex: “”,

email: “”,

address: “”,

},

  • showEditDialog 除了处理对话框显示,还绑定了修改用户对象。editUser 修改用户。

// 监听 修改用户状态

showEditDialog(userinfo) {

this.editDialogVisible = true;

console.log(userinfo);

this.editForm = userinfo;

},

//修改用户

editUser() {

userUpdate(this.editForm)

.then((res) => {

if (res.data.code === 200) {

this.editDialogVisible = false;

this.getUserList();

this.$message({

message: “修改用户成功”,

type: “success”,

});

} else {

this.$message.error(“修改用户失败”);

}

})

.catch((err) => {

this.$message.error(“修改用户异常”);

console.loge(err);

});

},

2.7、删除用户

  • api

//删除用户

export function userDelete(id) {

return request({

url: ‘/user/delete’,

method: ‘post’,

params: {

id

}

})

}

  • 在操作栏的作用域插槽里添加删除按钮,直接将作用域的id属性传递进去

<el-button

type=“primary”

size=“mini”

icon=“el-icon-edit”

@click=“showEditDialog(scope.row)”

<el-button

type=“danger”

size=“mini”

icon=“el-icon-delete”

@click=“removeUserById(scope.row.id)”

  • removeUserById 根据用户id删除用户

// 根据ID删除对应的用户信息

async removeUserById(id) {

// 弹框 询问用户是否删除

const confirmResult = await this.$confirm(

“此操作将永久删除该用户, 是否继续?”,

“提示”,

{

confirmButtonText: “确定”,

cancelButtonText: “取消”,

type: “warning”,

}

).catch((err) => err);

// 如果用户确认删除,则返回值为字符串 confirm

// 如果用户取消删除,则返回值为字符串 cancel

// console.log(confirmResult)

if (confirmResult == “confirm”) {

//删除用户

userDelete(id)

.then((res) => {

if (res.data.code === 200) {

this.getUserList();

this.$message({

message: “删除用户成功”,

type: “success”,

});

} else {

this.$message.error(“删除用户失败”);

}

})

.catch((err) => {

this.$message.error(“删除用户异常”);

console.loge(err);

});

}

},

效果:

image-20210126192208197

2.8、批量删除用户

  • api

//批量删除用户

export function userBatchDelete(data) {

return request({

url: ‘/user/delete/batch’,

method: ‘post’,

data

})

}

  • 在ElementUI表格组件中有一个多选的方式,手动添加一个el-table-column,设type属性为selection即可

image-20210126192421265

在表格里添加事件:

@selection-change=“handleSelectionChange”

下面是官方的示例:

export default {

data() {

return {

multipleSelection: []

}

},

methods: {

handleSelectionChange(val) {

this.multipleSelection = val;

}

}

}

这个示例里取出的参数multipleSelection结构是这样的,我们只需要id,所以做一下处理:

image-20210126193018008

export default {

data() {

return {

multipleSelection: [],

ids: [],

}

},

methods: {

handleSelectionChange(val) {

this.multipleSelection = val;

//向被删除的ids赋值

this.multipleSelection.forEach((item) => {

this.ids.push(item.id);

console.log(this.ids);

});

}

}

}

  • 接下来就简单了,批量删除操作直接cv上面的删除,改一下api函数和参数就可以了

//批量删除用户

async batchDeleteUser(){

// 弹框 询问用户是否删除

const confirmResult = await this.$confirm(

“此操作将永久删除用户, 是否继续?”,

“提示”,

{

confirmButtonText: “确定”,

cancelButtonText: “取消”,

type: “warning”,

}

).catch((err) => err);

// 如果用户确认删除,则返回值为字符串 confirm

// 如果用户取消删除,则返回值为字符串 cancel

if (confirmResult == “confirm”) {

//批量删除用户

userBatchDelete(this.ids)

.then((res) => {

if (res.data.code === 200) {

this.$message({

message: “批量删除用户成功”,

type: “success”,

});

this.getUserList();

} else {

this.$message.error(“批量删除用户失败”);

}

})

.catch((err) => {

this.$message.error(“批量删除用户异常”);

console.log(err);

});

}

效果:

image-20210126193403139

完整代码有点长,就不贴了,请自行查看源码。

六、总结

================================================================

通过这个示例,相信大家已经对 SpringBoot+Vue 前后端分离开发有了一个初步的掌握。

当然,由于这个示例并不是一个完整的项目,所以技术上和功能上都非常潦草😓

有兴趣的同学可以进一步地去扩展和完善这个示例。👏👏👏

源码地址:https://gitee.com/fighter3/springboot-vue-demo.git

参考:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

最后

编程基础的初级开发者,计算机科学专业的学生,以及平时没怎么利用过数据结构与算法的开发人员希望复习这些概念为下次技术面试做准备。或者想学习一些计算机科学的基本概念,以优化代码,提高编程技能。这份笔记都是可以作为参考的。

名不虚传!字节技术官甩出的"保姆级"数据结构与算法笔记太香了

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

s.ids);

});

}

}

}

  • 接下来就简单了,批量删除操作直接cv上面的删除,改一下api函数和参数就可以了

//批量删除用户

async batchDeleteUser(){

// 弹框 询问用户是否删除

const confirmResult = await this.$confirm(

“此操作将永久删除用户, 是否继续?”,

“提示”,

{

confirmButtonText: “确定”,

cancelButtonText: “取消”,

type: “warning”,

}

).catch((err) => err);

// 如果用户确认删除,则返回值为字符串 confirm

// 如果用户取消删除,则返回值为字符串 cancel

if (confirmResult == “confirm”) {

//批量删除用户

userBatchDelete(this.ids)

.then((res) => {

if (res.data.code === 200) {

this.$message({

message: “批量删除用户成功”,

type: “success”,

});

this.getUserList();

} else {

this.$message.error(“批量删除用户失败”);

}

})

.catch((err) => {

this.$message.error(“批量删除用户异常”);

console.log(err);

});

}

效果:

image-20210126193403139

完整代码有点长,就不贴了,请自行查看源码。

六、总结

================================================================

通过这个示例,相信大家已经对 SpringBoot+Vue 前后端分离开发有了一个初步的掌握。

当然,由于这个示例并不是一个完整的项目,所以技术上和功能上都非常潦草😓

有兴趣的同学可以进一步地去扩展和完善这个示例。👏👏👏

源码地址:https://gitee.com/fighter3/springboot-vue-demo.git

参考:

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-iYuDwphk-1712465732630)]
[外链图片转存中…(img-T8Tix86Z-1712465732630)]
[外链图片转存中…(img-Ay7gdgRU-1712465732630)]
[外链图片转存中…(img-TT2AVVkZ-1712465732631)]
[外链图片转存中…(img-LfeSIkjd-1712465732631)]
[外链图片转存中…(img-Ox98CNTm-1712465732631)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-kLY7FWCG-1712465732632)]

最后

编程基础的初级开发者,计算机科学专业的学生,以及平时没怎么利用过数据结构与算法的开发人员希望复习这些概念为下次技术面试做准备。或者想学习一些计算机科学的基本概念,以优化代码,提高编程技能。这份笔记都是可以作为参考的。

名不虚传!字节技术官甩出的"保姆级"数据结构与算法笔记太香了

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-tHRTWvOk-1712465732632)]

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值