【别再做XX外卖啦!和我从零到1编写Mini版Easy-ES】完成一个Mapper模型

【从零到1编写Mini版Easy-ES】完成一个Mapper模型

作者:沈自在

代码仓库:https://gitee.com/tian-haoran/mini-easy-es

本节教程分支:https://gitee.com/tian-haoran/mini-easy-es/tree/course_02_create_mapper/

⚠️注意:本项目会持续更新,直到功能完善

1 前置知识

1.1 Spring 相关

1.1.1 什么是 FactoryBean接口?

很多同学都知道BeanFactory接口,这个是大名鼎鼎的Spring中的核心接口,IOC的根本所在。而这个FactoryBean的作用是用来创建一类bean,它的源代码是这样的:

public interface FactoryBean<T> {
  // 获取 ObjectType 的一个对象
 	T getObject() throws Exception;
  
  // 当前实现类所要创建的对象类型
  Class<?> getObjectType();
  
  default boolean isSingleton() {
		return true;
	}
}

FactoryBean演示

上面这个图可以很简单的去概括这个接口的作用,就是要一个对象,然后给一个对象的逻辑。

1.1.1.1 小小的深入一点

对于 FactoryBean接口,其实还有一个子接口,叫做SmartFactoryBean

public interface SmartFactoryBean<T> extends FactoryBean<T> {
	// 最核心的一个方法 --> 如果说这里返回 true 那么则会在 Spring容器初始化的时候就将这个Bean实例化
  default boolean isEagerInit() {
		return false;
	}
}

下面这段代码则是对于SmartFactoryBean的迫切加载在Spring中的体现:

// 这段代码来自 DefaultListableBeanFactory -> 922 行
public void preInstantiateSingletons() throws BeansException {
		// 省略部分代码
		for (String beanName : beanNames) {
			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
				if (isFactoryBean(beanName)) {
					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
          // 核心点便是这里了
					if (bean instanceof FactoryBean) {
						FactoryBean<?> factory = (FactoryBean<?>) bean;
						boolean isEagerInit;
						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
							isEagerInit = AccessController.doPrivileged(
									(PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
									getAccessControlContext());
						}
						else {
							isEagerInit = (factory instanceof SmartFactoryBean &&
									((SmartFactoryBean<?>) factory).isEagerInit());
						}
						if (isEagerInit) {
							getBean(beanName);
						}
					}
				}
				else {
					getBean(beanName);
				}
			}
		}

	}
1.1.1.2 实战一把

对于FactoryBean的使用其实只分为俩步:

  • 实现FactoryBean接口
  • 将实现类注入Bean工厂

下面这段代码来自Easy ES的源码,同理也可以在Mybatis的底层代码中找到类似的设计(对SqlSession的FactoryBean),这样便相当于托管给实现类创建一类Bean的能力

@Component // 注入BeanFactory
public class MapperFactoryBean<T> implements FactoryBean<T> {


    private final Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }
		
    // 这个便是这个FactoryBean所要创建的类型
    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}

1.1.2 BeanDefinitionRegistryPostProcessor扩展点

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
 void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry var1) throws BeansException;
}

首先看一下BeanDefinitionRegistryPostProcessor的父类BeanFactoryPostProcessor,您可能对BeanDefinitionRegistryPostProcessor有些陌生,但想必对BeanFactoryPostProcessor一定不陌生吧,这个是在Spring容器刷新时,创建完BeanFactory后会调用的后置处理器

// 代码来自AbstractApplicationContext 545 行
public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
		
				StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
				// 嘿哥们,请注意,所有的BeanFactory后置处理器都是在这里被调用哒
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);
				beanPostProcess.end();

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}
	}

从上一步点进去之后就可以看到下面这段:

// 代码来自 AbstractApplicationContext 746行
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    // 再从 invokeBeanFactoryPostProcessors 这里点进去
		PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

		// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
		// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
		if (!NativeDetector.inNativeImage() && beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
			beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
			beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
		}
	}

接下来:

// 代码来自:PostProcessorRegistrationDelegate 78行
if (beanFactory instanceof BeanDefinitionRegistry) {
			BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
			List<BeanFactoryPostProcessor> regularPostProcessors = new ArrayList<>();
			List<BeanDefinitionRegistryPostProcessor> registryProcessors = new ArrayList<>();

			for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
        // 查出来所有的 BeanDefinitionRegistryPostProcessor 后置处理器
				if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
					BeanDefinitionRegistryPostProcessor registryProcessor =
							(BeanDefinitionRegistryPostProcessor) postProcessor;
					registryProcessor.postProcessBeanDefinitionRegistry(registry);
					registryProcessors.add(registryProcessor);
				}
				else {
					regularPostProcessors.add(postProcessor);
				}
			}
    }
 // 执行它们!!!!
 invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);

这便是这个扩展点的渊源啦

1.1.2.1 如何使用呢?
// 这是在手写Mini版本Easy Es 中初期的一段代码,用于替换Mapper的扫描和BeanDefinition
@Component
public class MapperScannerRegister implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 1. 扫描包
        Set<Class<?>> classes = ClassScanner.scanPackage("tax.szz.mini.test.mapper");
        for (Class<?> clazz : classes) {

            // 1. 创建 BeanDefinition
            RootBeanDefinition beanDefinition = new RootBeanDefinition(clazz);
            String beanClassName = clazz.getName();

            // 2. 设置 BeanName
            beanDefinition.setBeanClassName(beanClassName);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            beanDefinition.setBeanClass(MapperFactoryBean.class);

            registry.registerBeanDefinition(StrUtil.lowerFirst(clazz.getSimpleName()), beanDefinition);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

1.1.3 Spring boot 的 spring.factories 机制

Spring Bootspring.factories 配置机制类似于 Java SPI,工程代码中在 META-INF/spring.factories 文件中配置接口的实现类名称,然后 Spring Boot 在启动时扫描该配置文件并实例化配置文件中的Bean

比如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  tax.szz.mini.core.EsAutoConfiguration

Spring框架则会自己去扫描这个文件夹并把配置的这个类加载并且实例化

// 代码来自 SpringFactoriesLoader 95行 (如果你看过Dubbo SPI部分的源码的话会发现逻辑其实大差不差的)
 public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
		Assert.notNull(factoryType, "'factoryType' must not be null");
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
		}
		List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
		if (logger.isTraceEnabled()) {
			logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
		}
		List<T> result = new ArrayList<>(factoryImplementationNames.size());
		for (String factoryImplementationName : factoryImplementationNames) {
      // 同时在这里你会发现,Spring会讲这些扫描到的类一个一个全部实例化
			result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
		}
		AnnotationAwareOrderComparator.sort(result);
		return result;
	}

而这也是SpringBoot插件机制的来源(Starter)

2.2 动态代理相关

2.2.1 什么是动态代理?

动态代理是指在运行时创建代理对象的过程,而不是在编译时确定。JDK动态代理利用Java的反射机制,在运行时动态生成代理类和代理对象,从而实现代理功能。这意味着我们可以在运行时为任何接口创建代理对象,而无需手动编写代理类。

2.2.2 如何使用JDK动态代理?

使用JDK动态代理非常简单,只需遵循以下几个步骤:

  1. 定义一个接口:首先,我们需要定义一个接口,该接口将成为代理对象和被代理对象之间的契约。接口应包含代理对象和被代理对象共同的方法。
  2. 实现被代理类:创建一个实现接口的被代理类,该类将包含实际的业务逻辑。
  3. 创建InvocationHandler:实现InvocationHandler接口,并重写invoke方法。invoke方法将在代理对象的方法被调用时执行,我们可以在该方法中添加额外的逻辑。
  4. 创建代理对象:使用Proxy类的newProxyInstance方法创建代理对象。该方法接受三个参数:类加载器、接口数组和InvocationHandler对象。通过调用该方法,我们将得到一个实现了指定接口的代理对象。
  5. 使用代理对象:现在,我们可以使用代理对象来调用接口中定义的方法。代理对象会在调用方法时自动触发InvocationHandlerinvoke方法,从而允许我们在方法调用前后添加自定义的逻辑。

2.2.3 一个简单的案例

// 这段代码来自 Easy ES 中,可以直接 双击 shift 查到
// 而这段代码的作用就是对 Mapper 进行代理,同时可以在MapperFactoryBean(下面那个代码块)中也可以看到它的核心逻辑就是 (定义Mapper 接口 -> 创建 Mapper 代理)
public class EsMapperProxy<T> implements InvocationHandler, Serializable {

    private final Class<T> mapperInterface;

    public EsMapperProxy(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        BaseEsMapper<?> baseEsMapper = new BaseEsMapperImpl<>();
        return method.invoke(baseEsMapper, args);
    }
}

public class MapperFactoryBean<T> implements FactoryBean<T> {


    private final Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }

    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}

2.2.4 动态代理的优势和应用场景

使用JDK动态代理有以下几个优势:

  • 灵活性:动态代理允许我们在运行时为任意接口创建代理对象,无需手动编写代理类,从而提供了更大的灵活性。
  • 解耦合:代理模式可以将代理对象和被代理对象解耦,使得它们可以独立进行修改和扩展。
  • 横切关注点处理:动态代理可以用于处理横切关注点,例如日志记录、性能监控、事务管理等。我们可以通过在InvocationHandlerinvoke方法中添加相应的逻辑来实现这些功能。

JDK动态代理在以下场景中特别有用:

  • 日志记录:通过在InvocationHandler中添加日志记录逻辑,我们可以方便地记录方法的调用信息,用于调试和分析。
  • 事务管理:通过在InvocationHandler中添加事务管理逻辑,我们可以实现对方法的事务性控制,例如开启事务、提交事务、回滚事务等。
  • 权限控制:通过在InvocationHandler中添加权限验证逻辑,我们可以对方法的调用进行权限控制,以确保只有具备相应权限的用户才能执行特定操作。

2 Mapper 模型设计

首先我们去分析一下要创建一个Mapper的映射需要做哪些工作:

  • 第一:需要扫描到 Mapper
  • 第二:Mapper 只是一个接口,我们需要提供一个实际操作(肯定是动态代理啦)

工程结构如下:

.
| |____src
| | |____main
| | | |____resources
| | | |____java
|____pom.xml
|____mini-easy-es-core
| |____src
| | |____main
| | | |____resources
| | | | |____META-INF
| | | | | |____spring.factories
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____core
| | | | | | | | |____core
| | | | | | | | | |____BaseEsMapper.java
| | | | | | | | | |____BaseEsMapperImpl.java
| | | | | | | | |____proxy
| | | | | | | | | |____EsMapperProxy.java
| | | | | | | | |____register
| | | | | | | | | |____MapperFactoryBean.java
| | | | | | | | |____factory
| | | | | | | | | |____MapperScannerRegister.java
| | | | | | | | |____EsAutoConfiguration.java
|____mini-easy-es-test
| |____src
| | |____test
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____test
| | | | | | | | |____TestSpringApplication.java
| | | | | | | | |____api
| | | | | | | | | |____course_02
| | | | | | | | | | |____ApiTest.java
| | |____main
| | | |____java
| | | | |____tax
| | | | | |____szz
| | | | | | |____mini
| | | | | | | |____test
| | | | | | | | |____mapper
| | | | | | | | | |____DocumentMapper.java
| | | | | | | | |____document
| | | | | | | | | |____Document.java

2.1 抽象Mapper

众所周知,一个Mapper其实就是一个接口,比如我们在使用MybatisPlus时候,可能会去继承BaseMapper以获得一些基础功能比如:

  • selectOne()
  • save()
  • 。。。

那我们也借鉴这种思想去定义一个 BaseEsMapper

public interface BaseEsMapper<T> {
		// 粗浅定义一个方法去创建索引
    Boolean createIndex(String indexName);
}

接下来有了基础方法,那么肯定需要对方法进行实现啦,我们编写一个BaseEsMapperImpl对此进行实现

public class BaseEsMapperImpl<T> implements BaseEsMapper<T> {
    @Override
    public Boolean createIndex(String indexName) {
        System.out.println("创建 Index");
        System.out.println("indexName = " + indexName);
        return Boolean.TRUE;
    }
}

okok stop, 听我say一下

我们在用Mybatis Plus的时候是不是经常会有这样的写法:

public class ApiTest {

    @Autowired
    private UserMapper userMapper;
}

对,没错,Mapper是要注入到Spring容器当中的,只有这样,我们才可以用注解去自动注入

**(敲黑板)⚠️注意:**现在我们的BaseEsMapper还没有和我们将来自己的业务中的Mapper产生关系。那么缕一下思路,我们还差什么:

  • 业务Mapper要和BaseEsMapper产生一个关系
  • Mapper要注入到Spring中

Mapper绑定

那么这个绑定关系就要用动态代理来维持了,比如下面这种实现

// 我们对 Mapper 接口代理和 BaseMapper 产生一个代理绑定关系
public class EsMapperProxy<T> implements InvocationHandler, Serializable {

    private final Class<T> mapperInterface;

    public EsMapperProxy(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        BaseEsMapper<?> baseEsMapper = new BaseEsMapperImpl<>();
        return method.invoke(baseEsMapper, args);
    }
}

接下来就是要把这种关系交给Spring来维持:

所谓维持就分为俩个点:

  • Mapper对象的创建
  • Mapper的注入

首先解决第一个问题,创建一个Mapper对象怎么办,很明显这是一类对象的创建,这个特点不就和 FactoryBean 很类似嘛

// 这下当要去获取某个Mapper接口的时候,不久会调用getObject()拿到我们提供的Mapper和BaseMapper之间的绑定代理了嘛,神奇的一批
public class MapperFactoryBean<T> implements FactoryBean<T> {


    private final Class<T> mapperInterface;

    public MapperFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    @SuppressWarnings("all")
    public T getObject() throws Exception {
        EsMapperProxy<T> esMapperProxy = new EsMapperProxy<>(mapperInterface);
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, esMapperProxy);
    }

    @Override
    public Class<?> getObjectType() {
        return this.mapperInterface;
    }
}

那么现在不就剩下了最后一步——注册Mapper了嘛,请看下文~~

2.2 注册Mapper

对于注册Mapper一般会有俩个方法:

  • 自定义Scanner,比如去继承ClassPathBeanDefinitionScanner然后重写 **doScan()**方法
  • 实现BeanDefinitionRegistryPostProcessor,自己去扫描Mapper接口然后封装BeanDefinition注册

都可以解决问题,这里先暂且用BeanDefinitionRegistryPostProcessor顶着,这样逻辑会更清晰一点

public class MapperScannerRegister implements BeanDefinitionRegistryPostProcessor {

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 1. 扫描包
        Set<Class<?>> classes = ClassScanner.scanPackage("tax.szz.mini.test.mapper");
        for (Class<?> clazz : classes) {

            // 1. 创建 BeanDefinition
            RootBeanDefinition beanDefinition = new RootBeanDefinition(clazz);
            String beanClassName = clazz.getName();

            // 2. 设置 Bean的一些属性
            beanDefinition.setBeanClassName(beanClassName);
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
            beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
          // ⚠️注意这里(偷梁换柱),假设这里是UserMapper 那么这样不就会在获取 userMapper Bean的时候去调用MapperFactoryBean去拿对象啦,一环扣一环就这样建立了联系
            beanDefinition.setBeanClass(MapperFactoryBean.class);
						// 3. 注册 BeanDefinition
            registry.registerBeanDefinition(StrUtil.lowerFirst(clazz.getSimpleName()), beanDefinition);
        }
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    }
}

2.3 自动注册

resources中创建目录META-INF并且在该目录下添加文件spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  tax.szz.mini.core.EsAutoConfiguration
// 这样就完成了一个基本的自动加载
@Configuration
public class EsAutoConfiguration {

    @Bean
    public MapperScannerRegister mapperScannerRegister() {
        return new MapperScannerRegister();
    }
}

2.4 测试一下

下面是测试工程结构:

.
|____src
| |____test
| | |____java
| | | |____tax
| | | | |____szz
| | | | | |____mini
| | | | | | |____test
| | | | | | | |____TestSpringApplication.java
| | | | | | | |____api
| | | | | | | | |____course_02
| | | | | | | | | |____ApiTest.java
| |____main
| | |____java
| | | |____tax
| | | | |____szz
| | | | | |____mini
| | | | | | |____test
| | | | | | | |____mapper
| | | | | | | | |____DocumentMapper.java
| | | | | | | |____document
| | | | | | | | |____Document.java

public class Document {
}

public interface DocumentMapper extends BaseEsMapper<DocumentMapper> {

}
@Disabled
@SpringBootTest(classes = TestSpringApplication.class)
public class ApiTest {

    @Autowired
    private DocumentMapper documentMapper;

    @Test
    void test(){
        documentMapper.createIndex("hello");
    }
}
@SpringBootApplication
public class TestSpringApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestSpringApplication.class, args);
    }
}

那么执行这个测试的结果就是:

image-20231215215202577

  • 52
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Python-管理后台脚手架实现了RBAC(基于角色的访问控制)和一个简单的SQLAlchemy的映射工具基于Tornado。管理后台脚手架是一个基础的框架,用于快速搭建管理后台系统。下面我详细介绍一下这两个功能的实现。 首先,RBAC是一种常见的权限管理模型,它通过对用户、角色和权限进行关联,实现了灵活的访问控制。在管理后台脚手架中,RBAC的实现包括以下步骤: 1. 定义角色和权限:可以通过配置文件或者数据库定义角色和权限列表,例如:管理员、编辑、查看等。 2. 分配角色和权限:根据具体需求,将用户分配给相应的角色,同时为角色分配适当的权限。 3. 鉴权:在用户登录后,可以根据用户的角色和权限,通过一些逻辑判断来决定用户是否具有执行某个操作的权限。 其次,SQLAlchemy是Python中一个强大的ORM(对象关系映射)工具,它可以将数据库表映射为Python类,方便开发者使用面向对象的方式进行数据库操作。在管理后台脚手架中,SQLAlchemy的mapper工具用于简化数据库操作的代码,提高开发效率。具体实现包括以下步骤: 1. 定义模型类:通过定义Python类,使用SQLAlchemy提供的装饰器和字段类型,将数据库表映射为Python类,定义了表结构和字段类型等信息。 2. 创建数据库连接:通过SQLAlchemy提供的连接池机制,设置数据库连接参数,并创建数据库会话。 3. 数据库操作:通过调用模型类的方法,进行数据库的增删改查操作。SQLAlchemy的mapper工具将会自动创建SQL语句并执行。开发者无需手动编写SQL语句。 总之,使用Python-管理后台脚手架可以快速搭建一个具有RBAC权限管理和使用SQLAlchemy的mapper工具的管理后台系统。这些功能的实现大大简化了开发者的工作,提高了开发效率和系统的可维护性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沈自在-

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值