SpringDataMongoDB-3

SpringDataMongoDB-3

自定义Spring Data Repositories的实现

自定义Repositories

SpringData提供了各种各样的方法为了减少代码量,但是如果这些还不能满足我们的要求,可以自己提供repository的实现。两种方式

  1. 独立于SpringData,不适用他提供的一些方法。自己定义接口,自己写实现类。

    这种很简单,不依托SpringData,那它就是一个独立的Bean。按照正常的方式使用就好了

    接口

    public interface CustomerBookRepository extends MongoRepository<Book, String> {
        // 自己定义的方法,找前10条数据
        List<Book> findTop10ByName(String name);
    }
    

    实现类

    @Component
    public class CustomerBookRepositoryImpl implements CustomerBookRepository {
        // 利用它来做查询操作
        private MongoTemplate mongoTemplate;
    
        public CustomerBookRepositoryImpl(MongoTemplate mongoTemplate) {
            this.mongoTemplate = mongoTemplate;
        }
    
    	@Override
        public List<Book> findTop10ByName(String name) {
            Criteria condition_1 = Criteria.where("name_1").is(name);
            Query param = Query.query(condition_1).limit(10);
            return mongoTemplate.find(param, Book.class);
        }
    }
    
    
  2. 想要用SpringData的一些方法,自己想在这个基础上增加一些自己的方法。

    这种方式比较奇特,按照下面的步骤

    • 定义接口,继承于Repository接口,自定义方法。
    • 写接口实现类,但是这个实现类是不需要继承自定义接口的,方法的签名得一模一样。并且bean的名字为接口名字+Impl(默认是这样,也是可以通过Enable${store}Repositories注解中的repository-impl-postfix来改变,这只是后缀,前缀还是接口名字)。
    • 使用的时候正常注入接口使用。

定义接口

public interface CustomerBookRepository extends MongoRepository<Book, String> {
    List<Book> findTop10ByName(String name);
}

写接口实现类,但不要继承,方法的签名一样

@Component
public class CustomerBookRepositoryImpl {
    private MongoTemplate mongoTemplate;

    public CustomerBookRepositoryImpl(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }


    public List<Book> findTop10ByName(String name) {
        Criteria condition_1 = Criteria.where("name_1").is(name);
        Query param = Query.query(condition_1).limit(10);
        return mongoTemplate.find(param, Book.class);
    }
}

测试类


@SpringBootTest(classes = SpringdataMongodbApplication.class)
public class BookQueryMethodRepositoryTest extends BaseApplicationTests {
      @Autowired //引用接口,
    private CustomerBookRepository customerBookRepository;
    
     @Test
    public void testCustomerFind(){
        List<Book> go = customerBookRepository.findTop10ByName("go");
        Assert.isTrue(go.size()<=10);
    }
}

重写CRUDRepositories

可以重写CRUDRepositories接口的中的方法,在自定义的Repositories里面,只要方法的签名一致就可以。

还是上面例子中的接口,实现类中重写了findAll方法;

@Component
public class CustomerBookRepositoryImpl {
    private MongoTemplate mongoTemplate;

    public CustomerBookRepositoryImpl(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }


    public List<Book> findTop10ByName(String name) {
        Criteria condition_1 = Criteria.where("name_1").is(name);
        Query param = Query.query(condition_1).limit(10);
        return mongoTemplate.find(param, Book.class);
    }

    public List<Book> findAll() {
        System.out.println("CustomerBookRepositoryImpl.findAll");
        return mongoTemplate.findAll(Book.class);
    }
}

结果

在这里插入图片描述

增加抽象接口

上面的例子还是一个一个的来,要是想提供一个顶级的接口,或者说修改curdrepository 接口中的方法在所有的子类中都有作用。需要按照下面的来

  • 定义顶级接口,继承与curdRepository接口。
  • 增加方法
  • 写实现类,实现自定义接口,并且继承于SimpleMongoRepository(取决于具体的模块,比如Jpa就是SimpleJpaRepository)。
  • 在@Enable{store}Repositories(@EnableMongoRepositories)将实现类配置给repositoryBaseClass属性
  • 像前面的一样,自己写接口,继承于顶级接口,做操作。

定义接口,定义方法

@NoRepositoryBean
public interface BaseRepository<T, ID> extends CrudRepository<T, ID> {
     // 自定义方法
    List<T> baseCustomerQuery();
}

写实现类

​ 实现接口,继承SimpleMongoRepository,注意,这里的类名,和上一章节的没有关系,随便写。

public class BaseRepositoryImplfdfdfd<T, ID> extends SimpleMongoRepository<T, ID> implements BaseRepository<T,ID>{
    public BaseRepositoryImplfdfdfd(MongoEntityInformation<T, ID> metadata, MongoOperations mongoOperations) {
        super(metadata, mongoOperations);
    }

    public List<T> baseCustomerQuery() {
        System.out.println("BaseRepositoryImpl.baseCustomerQuery");
        return super.findAll();
    }
}

在@EnableMongoRepositories中配置

@SpringBootApplication
@EnableMongoRepositories(repositoryBaseClass = BaseRepositoryImplfdfdfd.class) // 配置
public class SpringdataMongodbApplication {

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

测试类

在测试类中正常使用

先定义接口

public interface BaseBookRepository extends BaseRepository<Book,String> {

}

在写测试类

public class BaseTest extends BaseApplicationTests {
    @Autowired
    private BaseBookRepository baseBookRepository;
    @Test
    public void testBaseFind0(){
        System.out.println(baseBookRepository.baseCustomerQuery().size());
    }
}

结果

在这里插入图片描述

还可以通过这种方式重写 CrudRepository中的方法,比如findAll方法。比如现在要重写,如果不打算提供抽象接口的话,直接继承于SimpleMongoRepository,重写他的findAll方法就好了

Spring Data MongoDB中还有一个很方便的工具类,MongoTemplate,上面说的这些Repository底层都是通过它来实现的,下面的章节来介绍一下它的一些使用方式

MongoTemplate使用

大多数的情况下,它都是在Spring的环境下使用的,它也支持单独使用,按照下面的方式

public class MongoApp {

  private static final Log log = LogFactory.getLog(MongoApp.class);

  public static void main(String[] args) throws Exception {
	// 需要通过MongoClients.来创建client,并且指定操作的数据库
    MongoOperations mongoOps = new MongoTemplate(MongoClients.create(), "database");
    mongoOps.insert(new Person("Joe", 34));

    log.info(mongoOps.findOne(new Query(where("name").is("Joe")), Person.class));

    mongoOps.dropCollection("person");
  }
}

需要通过MongoClients来创建MongoClient,指定数据库就可以使用了。这不是我们的重点,重点是介绍它的使用

是什么

  1. MongoTemplate是SpringData MongoDB中的核心类,提供CRUD和贴合MongoDB的操作(聚合,索引),并且提供了Document(MongoDB中的概念)和实体对象之间的映射操作。

  2. 它配置好了之后,就是线程安全的。

  3. Document和实体对象之间的转换是通过MongoConverter 接口来实现的,Spring默认提供的是MappingMongoConverter,当然,我们也可以自定义。

  4. 将MongoDB的错误转换成更加切合Spring的错误。

  5. 保留了原始的通过MongoClient来访问的操作。

保存,更新,删除操作

保存

有下面的几个保存的方法

在这里插入图片描述

在这里插入图片描述

save方法等于MongoDB语句中的save操作,遇到_id一样的会替换掉(但是不推荐使用,多数情况下还是使用insert开头的),在保存的时候如果主键字段没有值(_id字段对应的属性),会自动生成,但是这个字段的类型只能为String,ObjectId,BigInteger,在CRUD的时候,SpringData MongoDB会自动的转换(转换为ObjectId),如果转换不了,就不会转换。只用原始的值,如果不想要它的这种转化,在_id字段上用@MongoId注解来标注,表示不需要转换。比如:

需要转换

public class Student {
    private String id;
    private String name;
    private Integer age;
    private Double score;
}

不需要转换

public class Student {
    @MongoId
    private String id;
    private String name;
    private Integer age;
    private Double score;
}

insert开头的方法对应的是MongoDB中的insert,支持集合,单个对象的插入,后面的String参数或者Class<?>参数可以直接传递集合的名字,也可以和这个集合对应的实体对象.

测试实体类

@Data
@Accessors(chain = true)
@Document
@ToString
public class Student {
    private String id;
    private String name;
    private Integer age;
    private Double score;

    private Book book;
}

构建测试数据

    static List<Book> buildBooks(int count) {
        ArrayList<Book> param = new ArrayList<>();
        List<String> bookName = Arrays.asList("java", "go", "php", "c", "c++", "Mysql");
        ThreadLocalRandom current = ThreadLocalRandom.current();
        LocalDate today = LocalDate.now();
        for (int i = 0; i < 1000; i++) {
            String name = bookName.get(current.nextInt(bookName.size()));
            Book book = new Book().setCount(current.nextInt(1000))
                    .setName(name)
                    .setPrice(current.nextDouble(100))
                    .setPublishTime(today.minusDays(current.nextInt(30)))
                    .setCount(current.nextInt(1000))
                    .setShortName(name + ":" + name);
            param.add(book);
        }
        return param;
    }

    static List<Student> buildStudents(int count) {
        List<Book> books = buildBooks(count);
        ArrayList<Student> res = new ArrayList<>();
        List<String> names = Arrays.asList("小红", "小黄", "小蓝", "小绿", "小青", "小粉");
        ThreadLocalRandom current = ThreadLocalRandom.current();
        for (int i = 0; i < count; i++) {
            Student student = new Student()
                    .setId(String.valueOf(UUID.randomUUID()))
                    .setName(names.get(current.nextInt(names.size())))
                    .setAge(current.nextInt(100))
                    .setScore(current.nextDouble(100))
                    .setBook(books.get(current.nextInt(books.size())));
            res.add(student);
        }
        return res;
    }

例子:


	@Test
    public void testSave() {
        List<Student> students = buildStudents(100);
        // 插入多条数据,传递的是实体对象
        Collection<Student> students1 = mongoTemplate.insert(students, Student.class);
        Assert.isTrue(students1.size() == 100);
        //插入多条数据,指定集合名称
        Collection<Student> t_student = mongoTemplate.insert(students, "t_student");
        Assert.isTrue(t_student.size() == 100);
        //插入单个数据
        Student student = mongoTemplate.insert(students.get(0));
        Assert.notNull(student);
        //插入单个数据,指定集合的名称
        Student student_1 = mongoTemplate.insert(students.get(0),"t_student");
        Assert.notNull(student_1);

        // 分段式操作,可以选择Collection,还是用bulk来选择
        ExecutableInsertOperation.ExecutableInsert<Student> bookExecutableInsert = mongoTemplate.insert(Student.class);
        Collection<? extends Student> t_student1 = bookExecutableInsert.inCollection("t_student")
                .all(students);
        Assert.isTrue(t_student1.size() == 100);
    }
更新

在这里插入图片描述

这些都是和MongoDB的语法对应上的,也就是换了一种写法。命令的参数分为三个部分

  • 查询操作,(查询操作在后面会详细的说)
  • 更新操作
  • 实体对象。或者集合名称。
   @Test
    public void testUpdate() {
        Criteria criteria = Criteria.where("_id").is(new ObjectId("626bcdd98646873e6f83e94f"));
        Update update = Update.update("name", "name-test");
        UpdateResult updateResult = mongoTemplate.updateFirst(Query.query(criteria), update, Student.class);
        System.out.println(updateResult);
    }

主要说说Update对象

Update对象对应的是MongoDB 更新语句中的update块。MongoDB语法里面有的,Update对象里面也有。比如 s e t , set, setrename等,可以看Methods in the Update Class

在这里插入图片描述

在使用的时候可以直接创建Update对象,或者通过它的静态方法来创建Update.update(String key, @Nullable Object value)。比如下面的例子

  @Test
    public void testUpdate1() {
        Criteria criteria = Criteria.where("_id").is(new ObjectId("626bcdd98646873e6f83e94f"));
        Update update = new Update()
                .rename("name", "name_1")
                .currentDate("data_time");
        UpdateResult updateResult = mongoTemplate.updateFirst(Query.query(criteria), update, Student.class);
        Assert.isTrue(updateResult.getModifiedCount() > 0);
    }

对应的MongoDB的语句为

db.student.updateOne(
{
   _id:ObjectId("626bcdd98646873e6f83e94f")
},
{
    $currentDate:{
        data_time:true
    },
    $rename:{
        "name":"name_1"
    }
}
);

此还,还有findAndModify类似的方法,也都是和MongoDB一一对应。它还支持FindAndModifyOptions来设置属性,比如upsert,new等。

  @Test
    public void testUpdate2(){
        Criteria criteria = Criteria.where("_id").is(new ObjectId("626bcdd98646873e6f83e94f"));
        Update update = new Update()
                .currentDate("data_time");
        //设置new
        FindAndModifyOptions findAndModifyOptions = FindAndModifyOptions.options()
                .returnNew(true);
        Student andModify = mongoTemplate.findAndModify(Query.query(criteria), update, findAndModifyOptions, Student.class);
        System.out.println(andModify);
    }
删除

以remove开头

在这里插入图片描述

同样的,也是和mongoDB的语法是对应的

@Test
    public void testDelete0(){
        Criteria criteria = Criteria.where("name").is("小粉");
        DeleteResult remove = mongoTemplate.remove(Query.query(criteria), Student.class);
        System.out.println(remove);
    }

还可以使用update方法中的聚合操作 Aggregation Pipeline Updates

还可以使用更加精细的控制方式来更新 Finding and Replacing Documents

乐观锁

SpringData MongoDB也提供了JPA那样的乐观锁机制。使用@Version注解,每次数据更新版本号就会加1,当然版本号也是保存在数据库中的。如果当前更新的版本落后于数据库的版本就会报错(OptimisticLockingFailureException )。

@Data
@Accessors(chain = true)
@Document
@ToString
public class Student {
    @MongoId
    private String id;
    private String name;
    private Integer age;
    private Double score;

    private Book book;
    @Version
    private Long v;
}
    @Test
    public void testLock() {
        Student student = buildStudents(1).get(0); // 构建一条数据
        Student c = mongoTemplate.insert(student);   // 插入数据,version 还是0
        Student byId = mongoTemplate.findById(c.getId(), Student.class); // 查找刚刚加载的数据,版本还是0,
        c.setName("name111"); // 修改
        mongoTemplate.save(c);  // 保存,修改,这个时候version变为1了,save方法在id相等的时候会替换
        mongoTemplate.save(byId); //保存,但是这个版本是0,就会报错 OptimisticLockingFailureException
    }

查询

有两种方式

  1. 使用他们提供的流式的API
  2. 自己写json。
自己写json

构建BasicQuery对象,传递给mongoTemplate来查询

 @Test
     public void testFind1(){
        BasicQuery basicQuery = new BasicQuery("{\n" +
                "    name:\"小红\",\n" +
                "    \"book.count\":{$gt:50}\n" +
                "}");
        List<Student> students = mongoTemplate.find(basicQuery, Student.class);

        Criteria criteria = Criteria.where("name").is("小红")
                .andOperator(Criteria.where("book.count").gt(50));
        List<Student> students1 = mongoTemplate.find(Query.query(criteria), Student.class);
         
         // 两种方式 比对一下,看是否正确,,这里得重写equals和hashcode方法
        Assertions.assertEquals(students,students1);
    }

原始的mongoDB的查询语句

db.student.find(
{
    name:"小红",
    "book.count":{$gt:50}
}
)
使用他们提供的流式的API

查询分为两个部分,Query对象和Criteria。他们的方法名和MongoDB中的运算符的名字一样的。

Criteria中的方法如下:

Methods for the Criteria Class

Query中的方法如下:

Methods for the Query class

选择字段展示(Select)

操作Query对象中的Field的include()exclude()来选择字段

在这里插入图片描述

可以使用投影操作

query.fields()
  .project(MongoExpression.create("'$toUpper' : '$last_name'"))         
  .as("last_name");  
Distinct 操作
 @Test
    public void testFind2(){
        // 两种方式
        Criteria age = Criteria.where("age").gt(40);
        List<String> students = mongoTemplate.findDistinct(Query.query(age), "name", Student.class,String.class);

        
        //
        List<String> name = mongoTemplate.query(Student.class)
                .distinct("name")
                .matching(age)
                .as(String.class)
                .all();
        Assertions.assertArrayEquals(students.toArray(new String[]{}),name.toArray(new String[]{}));

    }

对应的MongoDB的语句

db.student.distinct(
"name",{age:{$gt:40}}
)
全文查询

具体的可以看 Full-text Queries

前提是得先创建好全文索引

Query query = TextQuery
  .queryText(new TextCriteria().matchingAny("coffee", "cake"))
  .sortByScore() 
  .includeScore(); 

List<Document> page = template.find(query, Document.class);
流式API

通过它可以更加灵活的做操作,在和更加的底层的交互的时候,可以通过它来做,在query,update,insert返回类里有不同的操作。

 @Test
    public void testFind4(){
        //查询
        long ageCount = mongoTemplate.query(Student.class)
                .matching(Query.query(Criteria.where("age").gt(10)))
                .count();
        System.out.println(ageCount);

        //跟新
        UpdateResult first = mongoTemplate.update(Student.class)
                .matching(Query.query(Criteria.where("age").gt(10)))
                .apply(new Update().inc("age", 1))
                .first();

        // 添加
        mongoTemplate.insert(Student.class)
                .one(buildStudents(1).get(0));
    }
通过Example查询(QBE)

Example由三个部分组成

  • Probe:Example关联的实体对象,实体对象里面有多个字段。
  • ExampleMatcher:用来做匹配的,包含如何匹配特定字段的详细信息。
  • Example:用来创建查询的,Example是由probe和ExampleMatcher组成的。

Example限制

  • 不支持嵌套查询或者分组的查询,比如firstname = ?0 or (firstname = ?1 and lastname = ?2)
  • 只支持start/contains/ends/regex 匹配和其他类型的精确匹配。

最后还是通过Example对象来构建Criteria对象,封装为Query对象来查询

包装类型如果没有值在查询的时候会忽略,基本类型会按照默认值来处理<

例子

通过mongoTemplate来使用

  @Test
    public void testFind5(){
        Student student = new Student()
                .setName("小红");
        Example<Student> of = Example.of(student);
        Student one = mongoTemplate.findOne(Query.query(Criteria.byExample(of)), of.getProbeType());
        System.out.println(one);
    }

通过QueryByExampleExecutor来使用

QueryByExampleExecutor是一个接口,自定义的接口需要实现这个接口,可以直接使用它提供的一些方便的方法。CrudRepository没有实现,但是MongoRepository实现了,主要我们的接口继承它,就可以直接用。

在这里插入图片描述

简单

  @Test
    public void testFind(){
        Student student = new Student()
                .setName("小红");
        Example<Student> of = Example.of(student);
        List<Student> all = studentCrudRepository.findAll(of);
        all.forEach(System.out::println);
    }

复杂

  @Test
    public void testFind(){
        Student student = new Student()
                .setName("小红");
        Example<Student> of = Example.of(student);
        // 通过Function做转换
        List<Student> all = studentCrudRepository.findBy(of, new Function<FluentQuery.FetchableFluentQuery<Student>, List<Student>>() {
            @Override
            public List<Student> apply(FluentQuery.FetchableFluentQuery<Student> studentFetchableFluentQuery) {
                Sort ascending = Sort.sort(Student.class).by(Student::getAge).ascending();
                return studentFetchableFluentQuery.sortBy(ascending)
                        .all();
            }
        });
        all.forEach(System.out::println);
    }

可以通过ExampleMatcher来做更加精细的控制,详细的看Example Matchers

  @Test
    public void testFind7(){
        Student student = new Student()
                .setName("小");
        ExampleMatcher matcher = ExampleMatcher.matching()
               // 给name字段,调整匹配,
                .withMatcher("name",matcher1 -> {
                     // name字段前缀开头。 这里能查到所有以小开头的name值
                    matcher1.startsWith();
                });

        Example<Student> of = Example.of(student,matcher);
        studentCrudRepository.findAll(of).forEach(System.out::println);
    }

聚合操作

聚合操作文档-MongoDB

聚合操作文档-SpringData MongoDB

三个核心概念

  • Aggregation

    代表的是MongoDB中的aggregate操作,通过它来表示聚合操作,它是由Aggregation的newAggregation(…)的静态方法来创建,可以将多个pipeline聚合在一块。

  • AggregationDefinition

    代表MongoDB pipeline 的操作。通过Aggregate来创建AggregateOperation。

  • AggregationResults

    聚合结果的返回值,它持有原始的返回值。

使用方式如下:

import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;

Aggregation agg = newAggregation(
    pipelineOP1(),
    pipelineOP2(),
    pipelineOPn()
);

AggregationResults<OutputType> results = mongoTemplate.aggregate(agg, "INPUT_COLLECTION_NAME", OutputType.class);
List<OutputType> mappedResult = results.getMappedResults();

SpringData MongoDB支持的聚合操作

Supported Aggregation Operations

例子:

mongoDB语法:

还是上面说的student的例子,先做投影,在做分组,分组是按照name和book.name分组,在由一些的函数操作。强烈推荐使用 Aggregation来创建pipline,聚合pipline

db.student.aggregate(
    {
        $project:{
            name_1:"$name",
            age:1,
            score:1,
            book:1
        }
    },
    {
        $group:{
            _id:{
                name:"$name_1",
                bookName:"$book.name"
            },
            count:{$sum:1},
            bookCount:{$sum:"$book.count"},
            priceArray:{$push:"$book.price"}
        }
    },
    {
        $sort:{
        bookCount:1
        }
    }
)

java代码

	@Test
    public void testFind8(){
        ProjectionOperation projectionOperation = Aggregation.project("age", "score", "book")
                .and("name").as("name_1");
        GroupOperation groupOperation = Aggregation.group("name_1", "book.name")
                .sum("book.count").as("bookCount")
                .count().as("count")
                .push("book.price").as("priceArray");

        SortOperation sortOperation = Aggregation.sort(Sort.by("bookCount").ascending());
        
        
        Aggregation aggregation = Aggregation.Aggregation(projectionOperation, groupOperation,sortOperation);
        AggregationResults<Map> student = mongoTemplate.aggregate(aggregation, "student", Map.class);
        List<Map> mappedResults = student.getMappedResults();
        mappedResults.forEach(System.out::println);
    }

写好每个阶段,通过Aggregation.Aggregation聚合将多个聚合在一块。Aggregation中聚合操作中的支持的每个阶段。很方便。

更多的例子看官网的样子,还得自己多来

Aggregation Framework Examples

索引操作

相关的操作被封装为IndexOperations,可以通过mongoTemplate来操作

创建索引

 @Test
    public void testFind9(){
        //创建
        mongoTemplate.indexOps(Student.class)
                .ensureIndex(new Index("age", Sort.Direction.ASC).named("idx_age"));
        // 查找
        ArrayList<Object> collect = mongoTemplate.indexOps(Student.class)
                .getIndexInfo()
                .stream()
                .collect(ArrayList::new, (t, t1) -> {
                    t.add(t1.getName());
                }, ArrayList::addAll);
        Assertions.assertTrue(collect.contains("idx_age"));
    }

指定Callbacks

可以通过execute 方法来直接对原生的api来操作,在Spring template 的这些类里面,基本的思想是,将所有的操作都通过execute的回调方法来执行,比如JdbcTemplate,这样做是为了确保异常和任何的资源可以在一个地方统一的管理和处理。

操作原生的API来获取索引的名字。

 @Test
    public void testFind10(){
        List<String> name = mongoTemplate.execute(Student.class, new CollectionCallback<List<String>>() {
            @Override
            public List<String> doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
                return Streamable.of(collection.listIndexes())
                        .stream()
                        .map(t -> t.getString("name"))
                        .collect(Collectors.toList());
            }
        });
        System.out.println(name);
    }

有关SpringDataMongoDB和MongoTemplate相关的操作就介绍到这里了,这些常用的API都是和MongoDB的命令对应的,这里列举的不全,这也不难,多写写就熟悉了。还有一些SpringData MongoDB的内容没有说,后面再说。


关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值