封装jdbc-完成简易版的orm框架

        前面我们使用JDBC以及一些设计模式来完成数据的持久化操作,还是有大量的sql语句以及设置等操作,针对这些持久化操作能否以一种操作对象的方式来完成呢,即对外隐藏jdbc的实现细节,对方法api调用者来说只需要以操作对象的方式来调用就可以了。为了达到此目标,设计简易版的ORM映射框架。

默认映射规则

     没有特殊说明,类的简单名称对应关系数据库的表明,如果不一致,我们自定义一个注解@Table对表名进行映射。同理,对象属性名称默认对应关系表的字段,如果不一致,我们自定义一个注解@Column对列明进行映射说明。同时我们还需要指定id的定义@Id用来映射关系表的主键

  • @Table的定义:用来映射类和表名的对应关系
package com.wise.tiger.annotation;  
  
import java.lang.annotation.*;  
  
/** 
 * 自定义注解 
 *      注解相当于配置说明,本身不具备任何功能,功能体现在程序中对该注解的处理 
 * 
 * 元注解 
 *      @Retention :注解保留的阶段 
 *              SOURCE:源代码阶段 
 *              CLASS:字节码阶段 
 *              RUNTIME:运行时阶段 
 *      @Target : 该注解可以贴在什么地方 
 *          value = {}:表示属性value取值是一个数组,如果只有一个取值,那么{} 
 *      @Documented 
 * 属性 
 *      数据类型 属性名(); 
 *      可以给属性指定默认值: String name() default "";使用注解时如果没有指定属性值,那么会取默认值 
 *      属性可以指定多个,如果只有一个属性,且该属性名为value,那么可以不用写value = 
 * 
 * 
 */  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
@Documented  
public @interface Table {  
    String value();//表名  
}  
  •  @Column的定义,用来映射属性和列名的对应关系
package com.wise.wise.annotation;  
  
import java.lang.annotation.*;  
  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
@Documented  
public @interface Column {  
    /** 
     * 属性名和数据库字段的映射 
     * @return 数据库表字段名称 
     */  
    String value();  
}  
  •  @Id的定义:用来定义主键id
package com.wise.wise.annotation;  
  
import java.lang.annotation.*;  
  
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.METHOD,ElementType.FIELD})  
@Documented  
public @interface Id {  
    /** 
     * id名称 
     * @return id名称 
     */  
    String value() default "id";  
} 

实体元数据的定义

Java代码 

package com.wise.domain;  
  
import com.wise.annotation.Column;  
import com.wise.annotation.Id;  
import com.wise.annotation.Table;  
  
import java.time.LocalDate;  
  
/** 
 * 图书实体 
 */  
@Table("tb_book")  
public class Book{  
    /** 
     * id 
     */  
    private Integer id;  
    /** 
     * 图书名称 
     */  
    private String title;  
    /** 
     * 图书作者 
     */  
    private String author;  
    /** 
     * 图书价格 
     */  
    private float price;  
    /** 
     * 出版社信息 
     */  
    private String publisher;  
    /** 
     * 图书简介 
     */  
    private String intro;  
    /** 
     * 出版日期 
     */  
    private LocalDate publishDate = LocalDate.now();  
  
    @Column("publish_date")  
    public LocalDate getPublishDate() {  
        return publishDate;  
    }  
  
    //******************setter and getter ****************//  
}  

设计持久化操作api

  • persist(T entity):将实体对象进行持久化(保存进数据库)
  • remove(Serializable id,Class<?> clazz):根据id删除对应的特定类型数据,具有缓存功能
  • merge(T entity):修改实体
  • T findById(Serializable id):根据主键(id)获取对应的实体信息
  • List<T> list(Class<T> clazz,int offset,int size):根据偏移量和每次加载记录数分页查询
  • long getCount(Class<?> clazz):获取特定类型总记录数

对外开放sql语句(本地sql)的两个api

  • executeUpdate
  • executeQuery
public class SqlSession {  
    /** 
     *  执行dml语句模板方法 
     * @param sql 传递的sql语句 
     * @param params 预编译语句的站位参数 
     * @return 影响数据行数 
     */  
    public int executeUpdate(String sql,Object... params){  
        var ret = 0;  
        try(var conn = DBHelper.getConnection();  
            var ptst = conn.prepareStatement(sql)){  
            for(int i = 0; params.length > 0 && i < params.length; i++)  
                ptst.setObject(i + 1,params[i]);  
            ptst.executeUpdate();  
        }catch (SQLException e){  
            e.printStackTrace();  
        }  
        return ret;  
    }  
  
    /** 
     * 执行dql语句模板方法 
     * @param sql 传递的sql语句 
     * @param handler 结果集处理handler(策略模式) 
     * @param params 预编译语句的站位参数 
     * @param <T> 参数化类型 
     * @return 查询结果 
     */  
    public <T> T executeQuery(String sql, ResultSetHandler<T> handler, Object... params){  
        T ret = null;  
        try(var conn = DBHelper.getConnection();  
            var ptst = conn.prepareStatement(sql)){  
            for(int i = 0; params.length > 0 && i < params.length; i++)  
                ptst.setObject(i + 1,params[i]);  
            var rs = ptst.executeQuery();  
            ret = handler.handler(rs);  
        }catch (SQLException e){  
            e.printStackTrace();  
        }  
        return ret;  
    }  
  
    /** 
     * 添加实体数据到数据库 
     * @param entity 实体信息 
     * @return 影响行数,后期可以返回自增的主键 
     */  
    public <T> int insert(T entity){  
        //生成具体的插入的sql语句  
        try {  
            var sp = insertSqlAndParams(entity);  
            System.out.println(sp.getSql());  
            System.out.println(Arrays.toString(sp.getParams()));  
            return executeUpdate(sp.getSql(),sp.getParams());  
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
        return 0;  
    }  
  
    /** 
     * 根据主键primary key(id)删除指定数据 
     * @param id 主键id 
     * @return 影响行数 
     */  
    public int delete(Serializable id,Class<?> clazz){  
        var sql = "DELETE FROM " + getTableName(clazz) + " WHERE " + getIdName(clazz) + " = ?";  
        return executeUpdate(sql,id);  
    }  
  
    /** 
     * 根据主键(id)修改对应的信息 
     * @param entity 修改好的实体信息 
     * @return 影响行数 
     */  
    public<T> int merge(T entity){  
        try {  
            var sp = updateSqlAndParams(entity);  
            return executeUpdate(sp.getSql(),sp.getParams());  
        } catch (IntrospectionException | InvocationTargetException  | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
        return 0;  
    }  
  
    /** 
     * 根据主键(id)获取对应的实体信息 
     * @param id id 
     * @param clazz 具体类型 
     * @return 
     */  
    public<T> T getById(Serializable id,Class<T> clazz){  
        var builder  = new StringBuilder("SELECT ");  
        try {  
            var pds = Introspector.getBeanInfo(clazz,Object.class).getPropertyDescriptors();  
            for(var pd : pds){  
                var method = pd.getReadMethod();  
                if(method.getAnnotation(Id.class) != null){  
                    builder.append(method.getAnnotation(Id.class).value()).append(',');  
                }else{  
                    builder.append(method.getAnnotation(Column.class) == null ? pd.getName() : method.getAnnotation(Column.class).value()).append(',');  
                }  
            }  
            var sql = builder.deleteCharAt(builder.length() - 1).append(" FROM ").append(getTableName(clazz)).append(" WHERE ").append(getIdName(clazz)).append(" = ?").toString();  
            return executeQuery(sql,new BeanHandler<>(clazz),id);  
        } catch (IntrospectionException e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  
  
    /** 
     * sql语句以及站位参数值 
     */  
    private class SqlAndParams{  
        //sql语句  
        private String sql;  
        //站位参数?对应的值  
        private Object[] params;  
        public String getSql() {  
            return sql;  
        }  
  
        public void setSql(String sql) {  
            this.sql = sql;  
        }  
  
        public Object[] getParams() {  
            return params;  
        }  
  
        public void setParams(Object[] params) {  
            this.params = params;  
        }  
    }  
  
    /** 
     * 生成具体实体entity的插入sql预编译语句以及?占位符参数值 
     *  INSERT INTO tb_name(fields...) VALUES(?,....?) 
     * @param entity 具体的javaBean实体 
     * @param <T> 具体的类型 
     * @return SqlAndParams 
     */  
    private<T> SqlAndParams insertSqlAndParams(T entity)  
            throws IntrospectionException,InvocationTargetException,IllegalAccessException {  
        var sp = new SqlAndParams();  
        var params = new ArrayList<>();  
        var builder = new StringBuilder("INSERT INTO ");  
        builder.append(getTableName(entity.getClass())).append('(');  
        var beanInfo = Introspector.getBeanInfo(entity.getClass(),Object.class);  
        var pds = beanInfo.getPropertyDescriptors();  
        for(var pd : pds){  
            //排除掉id的拼接  
            var method = pd.getReadMethod();  
            if(method.getAnnotation(Id.class) != null)continue;  
            //获取属性对应的属性名,属性名默认对应数据库里的字段名(非默认需处理)  
            var column = method.getAnnotation(Column.class);  
            var name = column != null ? column.value() : pd.getName();  
            builder.append(name).append(',');  
        }  
        builder.deleteCharAt(builder.length() - 1);//删除最后多余的,  
        builder.append(") VALUES(");  
        for(int i = 0; i < pds.length; i++){  
            //获取该属性对应的getter  
            var method = pds[i].getReadMethod();  
            //排除掉id的设置  
            if(method.getAnnotation(Id.class) != null)continue;  
            builder.append("?,");  
            params.add(method.invoke(entity));  
        }  
        builder.deleteCharAt(builder.length() - 1).append(')');  
        sp.setSql(builder.toString());  
        sp.setParams(params.toArray());  
        return sp;  
    }  
  
    /** 
     * 获取指定表名 Book/User/Topic/Reply... 
     * @param clazz 实体类型 
     * @return 表名,默认为类(类型)的简单名称 
     *  非默认需要额外处理: 
     *  简单名称        数据库表名 
     *  Book                tb_book 
     */  
    private String getTableName(Class<?> clazz) {  
       /* var tableName = clazz.getSimpleName(); 
        *//* 
         * 处理表名和类的简单名称不一致的情况 
         *//* 
        var table = clazz.getAnnotation(Table.class); 
        if(table != null) 
            tableName = table.value();*/  
        return clazz.getAnnotation(Table.class) == null ? clazz.getSimpleName() : clazz.getAnnotation(Table.class).value();  
    }  
  
    /** 
     * 获取id名称 
     * @param clazz 类型 
     * @return id名称 
     */  
    private String getIdName(Class<?> clazz){  
        String idName = "id";  
        try {  
            var pds = Introspector.getBeanInfo(clazz,Object.class).getPropertyDescriptors();  
            for(var pd : pds){  
                var id = pd.getReadMethod().getAnnotation(Id.class);  
                if(id != null) return id.value();  
            }  
        } catch (IntrospectionException e) {  
            e.printStackTrace();  
        }  
        return idName;  
    }  
  
    /** 
     * 更新的sql语句及预编译参数占位符值 
     * @param entity 更新后的实体信息 
     * @param <T> 类型 
     * @return 
     * @throws IntrospectionException 
     * @throws InvocationTargetException 
     * @throws IllegalAccessException 
     */  
    private<T> SqlAndParams updateSqlAndParams(T entity)  
            throws IntrospectionException,InvocationTargetException,IllegalAccessException {  
        var sp = new SqlAndParams();  
        var params = new ArrayList<>();  
        var builder = new StringBuilder("UPDATE ");  
        builder.append(getTableName(entity.getClass())).append(" SET ");  
        var beanInfo = Introspector.getBeanInfo(entity.getClass(),Object.class);  
        var pds = beanInfo.getPropertyDescriptors();  
        Object idValue = null;  
        for(var pd : pds){  
            //排除掉id的拼接  
            var method = pd.getReadMethod();  
            if(method.getAnnotation(Id.class) != null)  
                idValue = method.invoke(entity);  
            else {  
                //获取属性对应的属性名,属性名默认对应数据库里的字段名(非默认需处理)  
                var column = method.getAnnotation(Column.class);  
                var name = column != null ? column.value() : pd.getName();  
                builder.append(name).append(" = ?,");  
                params.add(method.invoke(entity));  
            }  
        }  
        builder.deleteCharAt(builder.length() - 1);//删除最后多余的,  
        builder.append(" WHERE ").append(getIdName(entity.getClass())).append(" = ?");  
        params.add(idValue);  
        sp.setSql(builder.toString());  
        sp.setParams(params.toArray());  
        return sp;  
    }  
} 

对结果集的处理参考前一篇博客里的策略模式,接下来添加缓存功能以及事务功能

//缓存池  
private Map<String, Object> cache = new HashMap<>();  
//可重入读写锁  
private ReadWriteLock rwl = new ReentrantReadWriteLock();  
  
   /** 
     * 添加实体数据到数据库 
     * @param entity 实体信息 
     * @return 影响行数,后期可以返回自增的主键 
     */  
    public <T> int insert(T entity){  
        var ret = 0;  
        //生成具体的插入的sql语句  
        try {  
            var sp = insertSqlAndParams(entity);  
            System.out.println(sp.getSql());  
            System.out.println(Arrays.toString(sp.getParams()));  
            ret = executeUpdate(sp.getSql(),sp.getParams());  
            var key = entity.getClass().getName() + "_" + getIdName(entity.getClass());  
            //将插入的数据添加进缓存池中  
            rwl.writeLock().lock();  
            try{  
                cache.put(key,entity);  
            }finally{  
                rwl.writeLock().unlock();  
            }  
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
        return ret;  
    }  
  
/** 
     * 根据主键primary key(id)删除指定数据 
     * @param id 主键id 
     * @return 影响行数 
     */  
    public int delete(Serializable id,Class<?> clazz){  
        var sql = "DELETE FROM " + getTableName(clazz) + " WHERE " + getIdName(clazz) + " = ?";  
        var ret = executeUpdate(sql,id);  
        var key = clazz.getName() + "_" + id;  
        //从缓存中移出删除的对象  
        rwl.writeLock().lock();  
        try{  
            cache.remove(key);  
        }finally{  
            rwl.writeLock().unlock();  
        }  
        return ret;  
    }  
  
/** 
     * 根据主键(id)修改对应的信息 
     * @param entity 修改好的实体信息 
     * @return 影响行数 
     */  
    public<T> int merge(T entity){  
        var ret = 0;  
        try {  
            var sp = updateSqlAndParams(entity);  
            ret = executeUpdate(sp.getSql(),sp.getParams());  
            rwl.writeLock().lock();  
            try{  
                cache.replace(entity.getClass().getName() + "_" + getIdName(entity.getClass()),entity);  
            }finally{  
                rwl.writeLock().unlock();  
            }  
        } catch (IntrospectionException | InvocationTargetException  | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
        return ret;  
    }  
  
/** 
     * 根据主键(id)获取对应的实体信息,先从缓存中去加载,没有找到再到数据库中去加载 
     * @param id id 
     * @param clazz 具体类型 
     * @return 
     */  
    public<T> T getById(Serializable id,Class<T> clazz){  
        var key = clazz.getName() + "_" +id;  
        rwl.readLock().lock();  
        Object value = null;  
        try{  
            value = cache.get(key);  
            if(value == null){  
                rwl.readLock().unlock();  
                rwl.writeLock().lock();  
                try{  
                    if(value==null){  
                       value = queryDB(id,clazz);  
                       if(value != null) cache.put(key,value);  
                    }  
                }finally{  
                    rwl.writeLock().unlock();  
                }  
                rwl.readLock().lock();  
            }  
        }finally{  
            rwl.readLock().unlock();  
        }  
        return (T)value;  
    }  
  
    /** 
     * 根据主键(id)获取对应的实体信息 
     * @param id id 
     * @param clazz 具体类型 
     * @return 
     */  
    private <T> T queryDB(Serializable id,Class<T> clazz){  
        var builder  = new StringBuilder("SELECT ");  
        try {  
            var pds = Introspector.getBeanInfo(clazz,Object.class).getPropertyDescriptors();  
            for(var pd : pds){  
                var method = pd.getReadMethod();  
                if(method.getAnnotation(Id.class) != null){  
                    builder.append(method.getAnnotation(Id.class).value()).append(',');  
                }else{  
                    builder.append(method.getAnnotation(Column.class) == null ? pd.getName() : method.getAnnotation(Column.class).value()).append(',');  
                }  
            }  
            var sql = builder.deleteCharAt(builder.length() - 1).append(" FROM ").append(getTableName(clazz)).append(" WHERE ").append(getIdName(clazz)).append(" = ?").toString();  
            return executeQuery(sql,new BeanHandler<>(clazz),id);  
              
        } catch (IntrospectionException e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
public<T> List<T> list(Class<T> clazz,int offset, int size){  
        List<T> list = null;  
        var builder  = new StringBuilder("SELECT ");  
        try {  
            var pds = Introspector.getBeanInfo(clazz, Object.class).getPropertyDescriptors();  
            for (var pd : pds) {  
                var method = pd.getReadMethod();  
                if (method.getAnnotation(Id.class) != null) {  
                    builder.append(method.getAnnotation(Id.class).value()).append(',');  
                } else {  
                    builder.append(method.getAnnotation(Column.class) == null ? pd.getName() : method.getAnnotation(Column.class).value()).append(',');  
                }  
            }  
            builder.deleteCharAt(builder.length() - 1).append(" FROM ").append(getTableName(clazz));  
            if(offset > 0 && size > 1) {  
                builder.append(" LIMIT ?,?");  
                list = this.executeQuery(builder.toString(),new ListBeanHandler<>(clazz),offset,size);  
            }else{  
                list = this.executeQuery(builder.toString(),new ListBeanHandler<>(clazz));  
            }  
              
        }catch (IntrospectionException e){  
            e.printStackTrace();  
        }  
        return list;  
    }  
    public<T> Long getCount(Class<T> clazz){  
        var sql = "SELECT COUNT(1) FROM " + getTableName(clazz);  
        return this.executeQuery(sql,rs -> rs.next() ? rs.getLong(1) : 0);  
    }  

 提供一个SqlSessionFactory工具类用来创建SqlSession对象,简单使用

public class BookDaoImpl implements BookDao {  
    private SqlSession template = SqlSession.buildSession();  
    @Override  
    public void persistent(Book book) {  
        template.insert(book);  
    }  
  
    @Override  
    public void delete(Integer id) {  
       template.delete(id,Book.class);  
    }  
  
    @Override  
    public void update(Book book) {  
        template.merge(book);  
    }  
  
    @Override  
    public Book search(Integer id) {  
       return template.getById(id,Book.class);  
    }  
  
    @Override  
    public List<Book> search() {  
        return template.list(Book.class,-1,-1);  
    }  
  
    @Override  
    public long getCount() {  
        return template.getCount();  
    }  
}  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一、SBORM 介绍 1、目前只考虑支持 mysql; 2、基于spring jdbc的上层封装,底层jdbc操作基于JdbcTemplate,对于使用spring jdbc的人会有一点价值,比较简洁的封装可以节省很多重复劳动,具体节省多少可以看看example; 3、实现一套简单的ORM(直接使用spring rowmapper,insert自己实现),可以基于对象进行crud和相对复杂(感觉比hibernate强大一点)的sql操作; 4、基于对象指定查询的字段,大部分时候可以忘掉结构进行业务开发; 5、支持简单的数据库路由,读写分离(半自动,需要指定取writer还是reader,默认规则reader采用随机的方式,当然也可以手动指定); 6、支持简单的分,主要是针对一定规则的分,比如百分、千分,也可以自己指定分后缀; 7、简单的单查询(比如所有条件是and或者or结构),基本实现0sql代码编写(类似HibernateTemplate selectByExample、findByCriteria、find等方法); 8、简单的单排序支持,支持多个排序条件组合; 9、对于复杂的sql查询,提供获取jdbctemplate实例进行操作,类似spring jdbc的常规用法; 10、提供Entity代码生成接口,Entity并非简单的pojo(尽可能不要去修改此类),引入字段常量类,方便查询的时候指定选择字段,从而更好实现查询条件的封装; 二、为什么写SBORM? 1、hibernate:过于臃肿,使用不够灵活,优化难(其实主要是因为很少用),HQL感觉就是个渣,在 mysql几乎一统天下的背景下,跨数据库级别的兼容吃力不讨好。Hibernate的对象化关联处理确实挺强大,但是使用起来坑太多,有多少人敢在项目 中大范围使用真不知道,屠龙刀不是人人都提的起啊。 2、mybatis:轻量级,基于xml的模式感觉不利于封装,代码量不小,基于xml维护也麻烦(个人观点, 现在注解模式貌似也挺不错),感觉mybatis更适合存在dba角色的年代,可以远离代码进行sql调优,复杂的查询拼装起来也更加优雅(java基本 就是if else ...),但是对于查询业务简单但是数据库集群环境的场景有点憋屈(其实对mybatis使用也不多,瞎评论^_^)。 3、spring jdbc:小巧,灵活,足够优秀,个人比较喜欢使用,但是代码量偏大,原生的接口重复劳动量大,比如insert、mapper之类的; SBORM只是针对spring jdbc的一些不方便的地方,做了一些封装,更加简化日常的开发工作,基于spring jdbc的RowMapper自动实现对象映射,也勉强算的上叫ORM,只是大部分功能已经由spring jdbc实现了。 平时不太喜欢使用hibernate和mybatis,主要是使用spring jdbc,写这个东西的出发点主要是平时使用spring jdbc觉 得比较麻烦,重复性的代码偏多,一方面通过自动mapper降低返回结果处理工作量,另一方面参考hibernate对象化查询条件的模式,写了一个 QueryBudiler,使得更多简单的单查询可以通过对象组织查询、更改逻辑,避免过多去写相似性的SQL语句,减少DAO接口量。 三、一些亮点 1、Entity的设计:很多人看了也许会说,这个不是POJO,不是纯粹的Java Bean,显得很另类。但是有多人在开发过程中(特别是在写sql的时候),经常要去看看结构设计?还有多少次因为改了某个字段,还得遍历去查找哪些 sql使用了这个字段?多少次看到在代码中直接传入字段名作为查询参数感到别扭?如果将结构字段都用java对象去描述,能够解决这些问题,就不必要在 乎是不是POJO了,后面看example的时候应该能体会这么做的一些好处,至少我觉得是挺方便的,将大部分查询脱离结构设计。 2、简单的数据库路由:如果分库结构不是太复杂(比如简单的读写分离、或者多个库集成),BaseDao可以自 动进行路由(比如读写分离,根据业务模式指定读、写库),如果非默认的路由规则,也可以通过手动设置的模式,进行数据库路由。数据库路由直接由 Entity指定,所有的路由都是根据Entity识别,也就是说查询也是围绕Entity展开的,避免类似使用spring jdbc的时候,各种 template实例跳来跳去,硬编码引入,写一个业务还得看看到底该用哪个template,尤其是多个数据库共用一个template实例的时候。 3、QueryBuilder:单查询基本上都可以实现零Sql(除非查询条件特别复杂的),更新、删除等操作也可以通过QueryBuilder进行批量处理,不局限于根据主键来处理。 4、分操作的支持:对于分操作和常规的使用没有区别,只是指定分规则,mybatis好像也可以通过制定参数实现分处理,没搞清楚hibernate对这个是怎么处理的(hibernate好像是bean和一对一绑定的)? 标签:sborm

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值