spring AOP切面实现多数据源、数据库切换

应用场景:

       对于数据量在1千万,单个mysql数据库就可以支持,但是如果数据量大于这个数时,那么查询的性能就会很低或是两个不同的数据库时。此时需要对数据库做水平切分,常见的做法是按照用户的账号进行hash,然后选择对应的数据库,以下是在springboot项目中利用AOP面向切面技术实现两个不同数据库之间的来回切换功能

一 配置数据源连接池  application.yml或application.properties

bootdo:
  uploadPath: /var/uploaded_files/
  httpPath: http://os.suyongw.com
logging:
  level:
    root: info
    com.bootdo: debug
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    primary:   注:主数据源连接池
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/bootdo?useUnicode=true&characterEncoding=utf8
      username: 
      password: 
      initialSize: 1
      minIdle: 3
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 30000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多个DruidDataSource的监控数据
      #useGlobalDataSourceStat: true


    secondary: 注:次数据源连接池
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/suyong?useUnicode=true&characterEncoding=utf8
      username: 
      password: 
      initialSize: 1
      minIdle: 3
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 30000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      filters: stat,wall,slf4j
      # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      # 合并多个DruidDataSource的监控数据
      #useGlobalDataSourceStat: true
  redis:
      host: localhost
      port: 6379
      password:
      # 连接超时时间(毫秒)
      timeout: 10000
      pool:
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 10
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 100
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        
mybatis: org.mybatis.spring.SqlSessionFactoryBean        

  水平切分图,数据入不同的库中

实现图:

  • 图1是比较常见的情况,单个数据库
  • 图2展示了web应用和数据库之间的一个中间层,这个中间层去选择使用哪个数据库

代码实现

配置数据源类:

package com.bootdo.common.config;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;  
import org.springframework.boot.context.properties.ConfigurationProperties;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.context.annotation.Primary;  
import org.springframework.jdbc.datasource.DataSourceTransactionManager;  
import org.springframework.transaction.PlatformTransactionManager;  
  
import javax.sql.DataSource;  
import java.util.HashMap;  
import java.util.Map;  

/**
 * @author Administrator
 *多数据源配置类
 */
@Configuration
public class DataSourceConfig {
    
    //数据源1  
    @Bean(name = "datasource1")  
    @ConfigurationProperties(prefix = "spring.datasource.primary") // application.properteis中对应属性的前缀  
    public DataSource dataSource1() {  
        return DataSourceBuilder.create().build();  
    }  
  
    //数据源2  
    @Bean(name = "datasource2")  
    @ConfigurationProperties(prefix = "spring.datasource.secondary") // application.properteis中对应属性的前缀  
    public DataSource dataSource2() {  
        return DataSourceBuilder.create().build();  
    }  
  
    /** 
     * 动态数据源: 通过AOP在不同数据源之间动态切换 
     * @return 
     */  
    @Primary  
    @Bean(name = "dynamicDataSource")  
    public DataSource dynamicDataSource() {  
        DynamicDataSource dynamicDataSource = new DynamicDataSource();  
        // 默认数据源  
        dynamicDataSource.setDefaultTargetDataSource(dataSource1());  
        // 配置多数据源  
        Map<Object, Object> dsMap = new HashMap<Object, Object>();  
        dsMap.put("datasource1", dataSource1());  
        dsMap.put("datasource2", dataSource2());  
  
        dynamicDataSource.setTargetDataSources(dsMap);  
        return dynamicDataSource;  
    }  
  
    /** 
     * 配置@Transactional注解事物 
     * @return 
     */  
    @Bean  
    public PlatformTransactionManager transactionManager() {  
        return new DataSourceTransactionManager(dynamicDataSource());  
    }  
/*    
    @Bean
    @ConfigurationProperties(prefix = "mybatis")
    public SqlSessionFactoryBean sqlSessionFactoryBean() {

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // Here is very important, if don't config this, will can't switch datasource

        // put all datasource into SqlSessionFactoryBean, then will autoconfig SqlSessionFactory

        sqlSessionFactoryBean.setDataSource(dynamicDataSource());

        return sqlSessionFactoryBean;

    }*/
    
}

 

默认数据源类:

package com.bootdo.common.config;

public class DataSourceContextHolder {
    /** 
     * 默认数据源 
     */  
    public static final String DEFAULT_DS = "datasource1";  
  
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();  
  
    // 设置数据源名  
    public static void setDB(String dbType) {  
        System.out.println("切换到{"+dbType+"}数据源");  
        contextHolder.set(dbType);  
    }  
  
    // 获取数据源名  
    public static String getDB() {  
        return (contextHo  lder.get());  
    }  
  
    // 清除数据源名  
    public static void clearDB() {  
        contextHolder.remove();  
    }  
}
 

自定义AOP切面类+数据自动切换类:

package com.bootdo.common.config;

import org.aspectj.lang.JoinPoint;  
import org.aspectj.lang.annotation.After;  
import org.aspectj.lang.annotation.Aspect;  
import org.aspectj.lang.annotation.Before;  
import org.aspectj.lang.reflect.MethodSignature;  
import org.springframework.stereotype.Component;

import java.lang.reflect.Method; 

/**
 * @author Administrator
 *自定义注解 + aop方式实现数据源动态切换
 */
@Aspect  
@Component  
public class DynamicDataSourceAspect {
      
    @Before("@annotation(DS)")  
    public void beforeSwitchDS(JoinPoint point){  
        //获得当前访问的class  
        Class<?> className = point.getTarget().getClass();  
        //获得访问的方法名  
        String methodName = point.getSignature().getName();  
        //得到方法的参数的类型  
        Class[] argClass = ((MethodSignature)point.getSignature()).getParameterTypes();  
        String dataSource = DataSourceContextHolder.DEFAULT_DS;  
        try {  
            // 得到访问的方法对象  
            Method method = className.getDeclaredMethod(methodName, argClass);  
            // 判断是否存在@DS注解  
            if (method.isAnnotationPresent(DS.class)) {  
                DS annotation = method.getAnnotation(DS.class);  
                // 取出注解中的数据源名  
                dataSource = annotation.value();  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        // 切换数据源  
        DataSourceContextHolder.setDB(dataSource);  
    }  
      
    @After("@annotation(DS)")  
    public void afterSwitchDS(JoinPoint point){  
        DataSourceContextHolder.clearDB();  
    }  
}
 

自定义类继承AbstractRoutingDataSource类,实现determineCurrentLookupKey()方法:

package com.bootdo.common.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource{

    @Override
    protected Object determineCurrentLookupKey() {
        //System.out.println("数据源为"+DataSourceContextHolder.getDB());  
        return DataSourceContextHolder.getDB();  
    }

}
 

自定义AOP切面接口类:

package com.bootdo.common.config;

import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  

/**
 * @author Administrator
 *自定义注解
 */
@Retention(RetentionPolicy.RUNTIME)  
@Target({ElementType.METHOD})   //用于描述方法
public @interface DS {
    String value() default "datasource1"; 
}

 

主函数入口引用:

package com.bootdo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.alipay.demo.trade.config.Configs;
@EnableTransactionManagement  // 启注解事务管理,等同于xml配置方式的 <tx:annotation-driven />

@ServletComponentScan
@MapperScan("com.bootdo.*.dao")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootdoApplication {
    public static void main(String[] args) {
         SpringApplication.run(BootdoApplication.class, args);
         Configs.init("zfbinfo.properties");
         System.out.println("ヾ(◍°∇°◍)ノ゙  启动成功      ヾ(◍°∇°◍)ノ゙\n" );
    }
}

 

上述代码中的注解说明:

  

自定义接口类注解:

注解@Retention可以用来修饰注解,是注解的注解,称为元注解。
Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS  RUNTIME   SOURCE
按生命周期来划分可分为3类:
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。
那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。
一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解,比如@Deprecated使用RUNTIME注解
如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;
如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,使用SOURCE 注解。

注解@Override用在方法上,当我们想重写一个方法时,在方法上加@Override,当我们方法的名字出错时,编译器就会报错
注解@Deprecated,用来表示某个类或属性或方法已经过时,不想别人再用时,在属性和方法上用@Deprecated修饰
注解@SuppressWarnings用来压制程序中出来的警告,比如在没有用泛型或是方法已经过时的时候

@Target:

   @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

  作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)

  取值(ElementType)有:

    1.CONSTRUCTOR:用于描述构造器
    2.FIELD:用于描述域
    3.LOCAL_VARIABLE:用于描述局部变量
    4.METHOD:用于描述方法
    5.PACKAGE:用于描述包
    6.PARAMETER:用于描述参数
    7.TYPE:用于描述类、接口(包括注解类型) 或enum声明

 

@EnableTransactionManagement 是Spring Boot 使用事务的注解 开启事务支持后,然后在访问数据库的Service方法上添加注解 @Transactional 便可

 

@ServletComponentScan 注解在 SpringBootApplication 上使用后,Servlet、Filter、Listener 可以直接通过 @WebServlet、@WebFilter、@WebListener 注解自动注册,无需其他代码。

Spring Boot MyBatis注解:@MapperScan和@Mapper

@MapperScan可以指定要扫描的Mapper类的包的路径 


@MapperScan("com.bootdo.*.dao")  //代表扫描多个包
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class BootdoApplication {
    public static void main(String[] args) {
         SpringApplication.run(BootdoApplication.class, args);
         Configs.init("zfbinfo.properties");
         System.out.println("ヾ(◍°∇°◍)ノ゙  启动成功      ヾ(◍°∇°◍)ノ゙\n" );
    }
}

@Mapper的话需要每个mapper接口类中添加 麻烦

       @Mapper  

   public interface DemoMapper {  

          @Insert("insert into Demo(name) values(#{name})")  

          @Options(keyProperty="id",keyColumn="id",useGeneratedKeys=true)  

          public void save(Demo demo); 

 }  

 

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) 注解的作用

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

该注解的作用是,排除自动注入数据源的配置(取消数据库配置),一般使用在客户端(消费者)服务中

 

@SpringBootApplication注解分析

首先我们分析的就是入口类Application的启动注解@SpringBootApplication,源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
....
}

发现@SpringBootApplication是一个复合注解,包括@ComponentScan,和@SpringBootConfiguration@EnableAutoConfiguration

  • @SpringBootConfiguration继承自@Configuration,二者功能也一致,标注当前类是配置类,并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名。
  • @EnableAutoConfiguration的作用启动自动的配置,@EnableAutoConfiguration注解的意思就是Springboot根据你添加的jar包来配置你项目的默认配置,比如根据spring-boot-starter-web ,来判断你的项目是否需要添加了webmvctomcat,就会自动的帮你配置web项目中所需要的默认配置。在下面博客会具体分析这个注解,快速入门的demo实际没有用到该注解。
  • @ComponentScan,扫描当前包及其子包下被@Component@Controller@Service@Repository注解标记的类并纳入到spring容器中进行管理。是以前的<context:component-scan>(以前使用在xml中使用的标签,用来扫描包配置的平行支持)。所以本demo中的User为何会被spring容器管理。

根据上面的理解,上面的入口类Application,可以使用:

package com.zhihao.miao;
import com.zhihao.miao.bean.User;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import java.util.Map;

@ComponentScan
public class Application {

    @Bean
    public Runnable createRunnable(){
        return () -> System.out.println("spring boot is running");
    }

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Application.class,args);
        context.getBean(Runnable.class).run();
        System.out.println(context.getBean(User.class));
        Map map = (Map) context.getBean("createMap");
        int age = (int) map.get("age");
        System.out.println("age=="+age);

    }
}

使用@ComponentScan注解代替@SpringBootApplication注解,也可以正常运行程序。原因是@SpringBootApplication中包含@ComponentScan,并且springboot会将入口类看作是一个@SpringBootConfiguration标记的配置类,所以定义在入口类Application中的Runnable也可以纳入到容器管理。

SpringBootApplication参数详解

图片.png

  • Class<?>[] exclude() default {}:
    根据class来排除,排除特定的类加入spring容器,传入参数value类型是class类型。
  • String[] excludeName() default {}:
    根据class name来排除,排除特定的类加入spring容器,传入参数value类型是class的全类名字符串数组。
  • String[] scanBasePackages() default {}:
    指定扫描包,参数是包名的字符串数组。
  • Class<?>[] scanBasePackageClasses() default {}:
    扫描特定的包,参数类似是Class类型数组。


在包下com.zhihao.miao.springboot定义一个启动应用类(加上@SpringBootApplication注解)

package com.zhihao.miao.springboot;


import com.zhihao.miao.beans.Cat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        Cat cat = context.getBean(Cat.class);
        System.out.println(cat);
    }
}

在com.zhihao.miao.beans包下定义一个实体类,并且想将其纳入到spring容器中,

public class Cat {
}
package com.zhihao.miao.beans;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public Cat cat(){
        return new Cat();
    }

}

启动启动类,打印结果如下:

 

说明Cat类并没有纳入到spring容器中,这个结果也如我们所想,因为@SpringBootApplication只会扫描@SpringBootApplication注解标记类包下及其子包的类(特定注解标记,比如说@Controller,@Service,@Component,@Configuration和@Bean注解等等)纳入到spring容器,很显然MyConfig不在@SpringBootApplication注解标记类相同包下及其子包的类,所以需要我们去配置一下扫包路径。

修改启动类,@SpringBootApplication(scanBasePackages = "com.zhihao.miao"),指定扫描路径:

package com.zhihao.miao.springboot;

import com.zhihao.miao.beans.Cat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(scanBasePackages = "com.zhihao.miao")
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        Cat cat = context.getBean(Cat.class);
        System.out.println(cat);
    }
}

启动并打印:

 

当然使用@SpringBootApplication(scanBasePackageClasses = MyConfig.class),指定scanBasePackageClasses参数的value值是你需要扫描的类也可以,结果一样,不过如果多个配置类不在当前包及其子包下,则需要指定多个。

再看一个列子,
在上面的列子的相同包下(com.zhihao.miao.springboot)配置了People,并将其纳入到spring容器中(@Component),我们知道@SpringBootApplication注解会扫描当前包及其子包,所以People类会纳入到spring容器中去,我们需要将其排除在spring容器中,如何操作?
可以使用@SpringBootApplication的另外二个参数(exclude或excludeName)

package com.zhihao.miao.springboot;

import org.springframework.stereotype.Component;

@Component
public class People {
}

启动类,

package com.zhihao.miao.springboot;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        People people = context.getBean(People.class);
        System.out.println(people);
    }
}

启动并打印结果:

 

然后修改@SpringBootApplication配置,

package com.zhihao.miao.springboot;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication(exclude = People.class)
public class Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext context =SpringApplication.run(Application.class,args);
        People people = context.getBean(People.class);
        System.out.println(people);
    }
}

很明显启动报错。使用@excludeName注解也可以。如下,
@SpringBootApplication(excludeName = {"com.zhihao.miao.springboot.People"})

 

 

 

 


 

        

        

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOP中,可以使用切面编程来实现动态数据源切换。下面是一个简单的示例: 首先,创建一个注解类`DataSource`,用于标识需要切换数据源的方法: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value() default "default"; } ``` 然后,创建一个切面类`DataSourceAspect`,在该类中定义切点和切面逻辑: ```java @Aspect @Component public class DataSourceAspect { @Around("@annotation(dataSource)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { try { // 获取要切换数据源名称 String dataSourceName = dataSource.value(); // 根据数据源名称切换数据源 switchDataSource(dataSourceName); // 执行目标方法 return joinPoint.proceed(); } finally { // 切换回默认数据源 switchDataSource("default"); } } // 实际切换数据源的逻辑 private void switchDataSource(String dataSourceName) { // 根据传入的数据源名称进行数据源切换逻辑的实现 // ... } } ``` 在上述代码中,`@Around("@annotation(dataSource)")`表示拦截带有`@DataSource`注解的方法,并执行切面逻辑。在切面逻辑中,首先获取切换数据源名称,然后根据该名称进行数据源切换操作。在目标方法执行完毕后,切面逻辑会将数据源切换回默认的数据源。 最后,使用`@DataSource`注解标识需要切换数据源的方法: ```java @Service public class UserService { @DataSource("db1") public void addUser(User user) { // 执行添加用户的逻辑 } @DataSource("db2") public void updateUser(User user) { // 执行更新用户的逻辑 } } ``` 在上述示例中,`addUser`方法使用名为"db1"的数据源,`updateUser`方法使用名为"db2"的数据源。 通过以上步骤,就可以使用Spring AOP实现动态数据源切换。当调用带有`@DataSource`注解的方法时,切面会根据注解中指定的数据源名称进行数据源切换,从而实现动态切换数据源的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值