为了账号安全,请及时绑定邮箱和手机立即绑定

Spring-Data-Jpa使用总结

标签:
Java SpringBoot

参考资源列表>> 1.官方文档:https : //docs.spring.io/spring-data/jpa/docs/2.1.5.RELEASE/reference/html/ > 2.“春数据JPA入门到精通”

前言

JPAJava持久性API '的简称,是'春天'在'休眠'的基础上进行的二次封装框架,为了更好更方便的融入'春天'大家庭,同时也提供了一些休眠SpringData` 没有的特性,与其他ORM框架一起构成,统一封装了ORM层,使开发人员使用起来更加方便快捷。

备注:本文所有代码都是基于SpringBoot 2.1.5版本

JPA的使用

基本单表操作的使用

对于操作单表来说,JPA提供了非常方便使用的封装,我们只需要按规范编写库接口同时继承JpaRepository就可以接受JPA基本功能了代码如下:

用户实体:

java 包 com 。一竹。实体; 进口龙目岛。建造者; 进口龙目岛。数据; 进口龙目岛。ToString ; 导入 javax 。坚持不懈。* ; 导入 java 。io 。可序列化; 导入 java 。实用程序。清单; 导入 java 。实用程序。设置;

@Entity @Table(名称= “T_USER”)@Data @Builder公共类用户器具序列化{私人静态最终长的的的serialVersionUID = 1L ; @Id @GeneratedValue(策略= GenerationType.IDENTITY)私人龙ID;私有字符串名称;专有字符串密码;私有整体时代;私人的整数性别; } 储存库接口:。java的包COM一竹仓库;引入玉米一竹实体用户。;公共接口UserRepository扩展了JpaRepository <用户,龙> { } 下面看看JPA为我们提供了一些替代操作单表数据的方法。java的包有机弹簧框架。数据。jpa。仓库; @NoRepositoryBean公共接口JpaRepository

< T , ID > 扩展了PagingAndSortingRepository < T , ID > , QueryByExampleExecutor < T > { List < T > findAll (); //查询全表数据 列表< T > findAll

(对var1排序); //查询全表数据,支持排序 List < T > findAllById ( Iterable < ID > var1 ); //根据id细分查询所有匹配数据< S 扩展T >列表< S > saveAll ( Iterable < S > var1 ); //批量保存或更新数据void flush (); //刷新本地缓存到数据库< S 扩展T > S saveAndFlush

 ( S var1 );  //保存或更新单挑数据及刷新本地缓存到数据库空隙deleteInBatch (可迭代< Ť > VAR1 ); //批量删除数据无效deleteAllInBatch (); //批量删除全表数据     T getOne ( ID var1 ); //根据id查询一条匹配数据< S 扩展T > List < S > findAll (示例< S > ar1 );//示例方式的查询指定实体<

  

  



   

S 扩展 T > List < S >  findAll (示例< S > var1 , Sort var2 ); 	//示例方式的查询指定实体并排序} ```

java 包 org 。弹簧框架。数据。仓库; @NoRepositoryBean 公共接口PagingAndSortingRepository < T , ID > 扩展CrudRepository < T , ID > { Iterable < T > findAll ( Sort var1 ); //根据排序查询全表,返回类型是任意集合 Page < T > findAll ( Pageable var1 ); //根据分页参数分页查询

} `“ java包org。弹簧框架。数据。仓库;@NoRepositoryBean公共接口CrudRepository < T,ID >扩展存储库< T,ID > { < S扩展T > S save(S var1);//保存或更新单条数据<S扩展T>可重复 saveAll( 可迭代< S > var1); //批量保存或更新可选 findById(ID var1); //根据id查询数据,,返回类型是可选的boolean existById(ID var1); //根据id判断数据是否存在Iterable findAll();//查询全表数据,返回类型为集合Iterable findAllById(Iterable var1);//根据id集合查询数据long count();//统计全表数据量void deleteById(ID var1);//根据ID删除数据无效删除(ŤVAR1);//删除单条数据void deleteAll(可迭代<?扩展T>

    		//删除指定集合数据无效的deleteAll();//删除全表数据} ```的java包有机弹簧框架数据仓库查询。。;公共接口QueryByExampleExecutor < Ť > { <小号扩展Ť >可选<小号> findOne(示例<小号> VAR1); //根据示例查询一条<S扩展T>可迭代<S> findAll(示例< S > var1);//根据示例查询所有数据<S扩展T>可重复<S> findAll(示例<S> var1,对var2排序); //根据示例查询所有数据,并排序<S扩展T>页面<S> findAll (示例<S> var1,可分页的var2);//根据示例分页查询<S     扩展T > long   count(示例<

; ///根据示例统计<S扩展T>布尔值存在(示例 var1); //根据示例判断数据是否存在} 另外,jpa提供了一套新的生成sql的机制,非常方便好用,JPA根据存储库接口的方法中的关键字,实体替换及出入参,自动生成SQL,这种方式启动容器的时候就可以检查语法是否正确,简单使用示例如下:的Java 包融为一体。一竹。仓库; 导入 com 。一竹。实体。用户; 导入组织。弹簧框架。数据。jpa 。仓库。实体图; 导入组织。弹簧框架。数据。jpa。仓库。JpaRepository

; 导入组织。弹簧框架。数据。jpa 。仓库。JpaSpecificationExecutor ; 导入组织。弹簧框架。数据。jpa 。仓库。查询; 导入组织。弹簧框架。数据。仓库。查询。参数; 导入 java 。

util.List;

public interface UserRepository extends JpaRepository<User, Long>{

/**
 * 根据年龄查询用户信息
 * @param age
 * @return
 */
List<User> findAllByAge(Integer age);

/**
 * 根据用户性别和所属组织名称查询用户信息
 * @param userSex
 * @param orgName
 * @return
 */
List<User> findBySexAndOrg(@Param("sex") Integer sex, @Param("name") String name);

/**
 *根据用户名模糊查询
 * @return 
 * / 
 列出<用户> findAllByNameLike(@Param(“名称”)字符串名称);} ``除了`发现`,`由`,`和`之外,还有一些关键字,全部定义在`PartTree`,`部分'类,组装起来可以生成各种各样的SQL,下面截取部分代码,研究员的同学可以打开原始码去阅读```java package org 。弹簧框架。数据。仓库。查询。解析器; 公共类PartTree 实现Streamable < PartTree 。OrPart > { 私有静态最终字符串KEYWORD_TEMPLATE =







 
   “(%s)(?=(\\ p {Lu} | \\ P {InBASIC_LATIN}))” ;私有静态最终字符串QUERY_PATTERN = “查找|读取|获取|查询|流” ; 私有静态最终字符串COUNT_PATTERN = “ count” ; 私有静态最终字符串EXISTS_PATTERN = “ exists” ; 私有静态最终字符串DELETE_PATTERN = “ delete | remove” ; 私有静态最终模式PREFIX_TEMPLATE = Pattern 。编译(
   
   
   
   
  “ ^(查找|读取|获取|查询|流|计数|存在|删除|删除)(((\\ p {Lu}。*?))??通过” );私有最终 PartTree 。学科学科; 私有最终 PartTree 。谓词谓词; 。。。私有静态 String [ ] split ( String text , String关键字){         模式模式=模式。编译(字符串。格式(“(%s)(?=(\\ p {Lu} | \\ P {InBASIC_LATIN}))” ,

关键字)); 返回模式。分割(文字); } 私人静态类谓词工具可流< PartTree 。OrPart > { 私有静态最终模式ALL_IGNORE_CASE =模式。编译(“ AllIgnor(ing | e)Case” ); 私有静态最终字符串ORDER_BY = “ OrderBy” ; 私人决赛

     列出< PartTree 。OrPart >节点; 私有最终 OrderBySource orderBySource ; 私有boolean alwaysIgnoreCase ; 公共谓词(字符串谓词,类< ?> domainClass ){             字符串[ ] parts = PartTree 。分裂(此。detectAndSetAllIgnoreCase (谓词),“排序依据” )

; 如果(部分。长度> 2){抛出新的IllegalArgumentException(“在方法名称中不能多次使用OrderBy!”));}其他{这个。例程=(列表)副本。流(PartTree。分割(份[0],“或”))。。过滤器(StringUtils的::

              hasText).map((part) -> {
                return new PartTree.OrPart(part, domainClass, this.alwaysIgnoreCase);
            }).collect(Collectors.toList());
            this.orderBySource =零件。length String > subject {                  匹配器GRP = LIMITED_QUERY_TEMPLATE。匹配(它);返回可选< String > subject , Pattern pattern ){== 2 ? new OrderBySource(parts[1], Optional.of(domainClass)) : OrderBySource.EMPTY;
        }
    }
    ...
}

private static class Subject {
    private static final String DISTINCT = "Distinct";
    private static final Pattern COUNT_BY_TEMPLATE = Pattern.compile("^count(\\p{Lu}.*?)??By");
    private static final Pattern EXISTS_BY_TEMPLATE = Pattern.compile("^(exists)(\\p{Lu}.*?)??By");
    private static final Pattern DELETE_BY_TEMPLATE = Pattern.compile("^(delete|remove)(\\p{Lu}.*?)??By"); 私有静态最终字符串LIMITING_QUERY_PATTERN = “(First | Top)(\\ d *)?” ; 私有静态最终模式LIMITED_QUERY_TEMPLATE =模式。编译(“ ^(find | read | get | query | stream)(Distinct)?(First | Top)(\\ d *)?(\\ p {Lu}。*?)?? By” ); 私人最终布尔不同; 私有最终布尔计数; 私有最终布尔存在; 私有最终布尔删除;
       
      
      
      
      
      
    private  final可选的< Integer > maxResults ; 公共主题(可选< ){ this 。独特= (布尔)主题。映射((它)- > { 返回它。包含(“鲜明” ); } )。orElse (假); 这个。数=

      
           
            
        
         this.matches(subject, COUNT_BY_TEMPLATE);
        this.exists = this.matches(subject,EXISTS_BY_TEMPLATE); 
        this.delete = this.matches(subject,DELETE_BY_TEMPLATE); 
        这个。maxResults= this。returnMaxResultsIfFirstKSubjectOrNull(主题);} private任选<Integer> returnMaxResultsIfFirstKSubjectOrNull(任选<String> subject){返回主题。映射((它)-> 
    
             !grp.find()?null:StringUtils.hasText(grp.group(4))吗?Integer.valueOf(grp.group(4)):1;             });         } 		...         private布尔匹配(返回)。映射((它)-> {返回图案。匹配(它)。发现();})。否则容易(假); }}}```

java包org。弹簧框架。数据。仓库。查询。解析器; 公共类Part {专有静态最终模式IGNORE_CASE =模式。编译(“ Ignor(ing | e)情况”);私有最终PropertyPath propertyPath; 私人最后部分。类型类型; 公共静态枚举类型{BETWEEN(2,新的字符串[] {“ IsBetween”,“之间”}),IS_NOT_NULL(0,新的字符串[] {“ IsNotNull”,”。NotNull”}), IS_NULL(0,新的字符串[] {“ IsNull”,“ Null”}),LESS_THAN(新字符串[] {“ IsLessThan”,“一个不超过”)),LESS_THAN_EQUAL(新字符)串[] { “ IsLessThanEqual”,“ LessThanEqual”}),GREATER_THAN(新字符串[] {“ IsGreaterThan”,“大于”}),GREATER_THAN_EQUAL(新变量[] {“ IsGreaterThanEqual”,“ GreaterThanEqual”}),BEFORE(新字符)串[] {“ IsBefore”,“ Before”}),AFTER(新字符串[] {“ IsAfter”,“ After”}),LIKE(新字符串[] {“ IsLike”,“ Like”})) ,, STARTING_WITH(新字符串[] {“ IsStartingWith”,“ StartingWith”,“ StartsWith”}),ENDING_WITH(新字符串[] {“ IsEndingWith”,“ EndingWith”,“ EndsWith”}),IS_NOT_EMPTY(0,新的弦[] {“ I sNotEmpty”,“ NotEmpty”}),

    IS_EMPTY(0,新的字符串[] {“ IsEmpty”,“ Empty”})),
    NOT_CONTAINING(新字符串[] {“ IsNotContaining”,“ NotContaining”,“ NotContains”}},包含
    (新的字符串[] {“ IsContaining”,“ Containing”,“包含”}),
    NOT_IN(新的字符串[] {“ IsNotIn”,“ NotIn”}),
    IN(新字符串[] {“ IsIn”,” In “}}),
    NEAR(新字符串[] {“ IsNear”,“ Near”}”),
    WITHIN(新字符串[] {“ IsWithin”,“ Inside”}),
    REGEX(新字符串[] { “ MatchesRegex”,“ Matches”,“ Regex”}),
    EXISTS(0,新字符串[] {“ Exists”}),
    TRUE(0,新字符串[] {“ IsTrue”,“ True”}),
    FALSE(0,新字符串[] {“ IsFalse”,“ False”}),
    NEGATING_SIMPLE_PROPERTY(新字符串[] {“ IsNot”,“ Not”}),SIMPLE_PROPERTY(新字符串[] {“是”,'等于'});私人静态最终列表<部分类型> ALL =副本.asList( IS_NOT_NULL,IS_NULL,不到LESS_THAN,LESS_THAN_EQUAL,GREATER_THAN,GREATER_THAN_EQUAL 
      

      之前,之后,NOT_LIKE,LIKE,STARTING_MPITH,ENDING_EITH,TY_IST_T,NOT_IN,IN,NEAR ININ,TY_TY,TY,TY,PER,TY,PER,PER 
      长度> ALL_KEYWORDS ;私有最终列表<String>关键字; 私有final int numberOfArguments; 静态。List{String> allKeywords = new ArrayList();。。。。。继承器var1 = ALL。继承器(); 而(VAR1。具有下一个()){部分。型型=(部分。类型)VAR1。下一个();                                                                                                                                                                                        
     
      
	
     

            allKeywords。addAll(类型。关键字);} ALL_KEYWORDS =集合。unmodifiableList(allKeywords);}} }```

可以看到单表操作的大部分需求JPA都为我们提供了现成的实现,但也支持我们使用@Query注解自定义查询SQL,方便有SQL基础的同学使用,SQL可控性强。

java包com。一竹。仓库; 引入com。一竹。实体。用户; jpa。仓库。实体图;导入组织。弹簧框架。数据。jpa。仓库。JpaRepository;导入组织。弹簧框架。数据。导入组织。弹簧框架。数据

。jpa。仓库。JpaSpecificationExecutor;导入组织。弹簧框架。数据。jpa。仓库。查询;导入组织。弹簧框架。数据。仓库。查询。参数; 引入java。实用程序。清单; 公共接口UserRepository扩展了JpaRepository <User,

 Long>{

/**
 * 查询所有用户信息
 * @return
 */
@Query(value = "from User u")
List<User> findAll();

/**
 * 根据年龄查询用户信息
 * @param age
 * @return
 */
@Query(value = "select * from t_user u where u.user_age = ?1", nativeQuery = true)
List<User> findAllByAge(Integer age); / **      *根据用户性别和所属组织名称查询用户信息     * @param userSex      * @param orgName      * @return      * / @Query ( value = “从用户u中选择u从u左加入u.org o其中u.userSex =: userSex和o.orgName =:orgName“ )     列表<用户> findUsersBySexAndOrg (@Param (” userSex“ )整数userSex ,@Param (” orgName“ )字符串orgName ); } ```

多表关联

@OneToOne@OneToMany@ManyToOne@ManyToMany


 
   




     



     
    
    
    

     
     
    

     
     


     
     
    

     
     
    

     
     
    长orgId; @ApiModelProperty(值=“用户信息”)@OneToOne @JoinColumn(名称=“ id”,可更新= false,可插入= false)私有UserInfo userInfo;@ApiModelProperty(值=“用户所属组织”)@ManyToOne @JoinColumn(名称=“ org_id”,可更新= false,可插入= false)    私人组织机构;@ApiModelProperty(值=“用户角色”)@OneToMany @JoinColumn(名称=“ USER_ID”,referencedColumnName =“ ID”,可插入=假,可更新=假)@NotFound(动作= NotFoundAction。IGNORE)私有集<角色>角色; @ApiModelProperty(值    =“用户工作”)@ManyToMany @JoinTable(名称=“ t_user_job”,joinColumns = @JoinColumn(名称=“ user_id”,referencedColumnName =“ id”),inverseJoinColumns = @JoinColumn(名称=“ job_id”), referencedColumnName =“ id“))@ NotFound(动作= NotFoundAction。。)IGNORE)    私人集合<Job>工作;```

     
    
       
    

     
    
       


     
    
        
    
    


    
    
 
   
   
    
    



>`private Set <角色>角色;和和私订<工作>的工作;“不能用同时使用”列表代替,会报错`org.hibernate.loader.MultipleBagFetchException:不能同时获取多个袋:[com.yizhu.entity.User.jobs,com.yizhu.entity.User.roles] ` ###动态查询```java 包 com 。一竹。仓库; 导入 com 。一竹。dto 。UserQueryDto ; 导入 com 。一竹。实体。组织; 导入 com 。一竹。实体。用户; 导入组织。









弹簧框架。数据。jpa 。域。规格; 导入 javax 。坚持不懈。标准。加盟; 导入 javax 。坚持不懈。标准。JoinType ; 导入 javax 。坚持不懈。标准。谓词; 导入 java 。实用程序。ArrayList ; 导入 java 。实用程序。





清单; 导入 java 。实用程序。可选的; 公共类UserSpecs { 公共静态规范<用户> listQuerySpec ( UserQueryDto userQueryDto ){ 返回(根,查询,构建器)- > {             列表<谓词>谓词= 新ArrayList < > ();


   

      
           
  

            可选的。ofNullable ( userQueryDto 。的getId ())。ifPresent (我- >谓词。添加(助洗剂。等于(根。获得(“ID” ),我)));             可选的。ofNullable ( userQueryDto 。的getName ())。ifPresent ( ñ -
>谓词。添加(建设者。等于(根。获得(“名称” ), ñ ))); 
            可选的。ofNullable ( userQueryDto 。getAge ())。ifPresent (一- >谓词。加(建设者。等于(根。获得(“时代” ), a))); 
            可选的。ofNullable ( userQueryDto 。getOrgId ())。ifPresent ( OI - >谓词。添加(助洗剂。等于(根。获得(“ORGID” ), OI )));             可选的。ofNullable ( userQueryDto 。getOrgName ())。
ifPresent(on -> {
                Join<User, Organization> userJoin = root.join(root.getModel().getSingularAttribute("org", Organization.class), JoinType.LEFT);
                predicates.add(builder.equal(userJoin.get("orgName"), on));
            });

            return builder.and(predicates.toArray(new Predicate[predicates.size()]));
        };
    }
}
package com.yizhu.service;

import com.yizhu.dto.UserQueryDto;
import com.yizhu.entity.User;
import com.yizhu.repository.UserRepository;
import com.yizhu.repository.UserSpecs;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public List<User> findUsersDynamic(UserQueryDto userQueryDto){
        return userRepository.findAll(UserSpecs.listQuerySpec(userQueryDto));
    }
}

审计功能使用

在启动类添加@EnableJpaAuditing注解表示开启jpa审计功能

package com.yizhu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class DemoApplication {

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

在需要使用审计功能的实体类添加@EntityListeners(AuditingEntityListener.class)注释解

package com.yizhu.entity;

import lombok.Builder;
import lombok.Data;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Entity
@Table(name = "t_role")
@Data
@Builder
@EntityListeners(AuditingEntityListener.class)
public class Role implements Serializable {
    private static final long serialVersionUID=1L;

    @ApiModelProperty(hidden = true)
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String roleName;

    @CreatedDate
    private Date createTime;
    
	@CreatedBy
    private Long createId;
    
    @LastModifiedDate
    private Date updateTime;
    
	@LastModifiedBy
    private Long updateId;
}

实现AuditorAware接口,告诉容器当前登录人id

package com.yizhu.configuration;

import org.springframework.data.domain.AuditorAware;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

public class UserAuditorAwareImpl implements AuditorAware<Long> {
    @Override
    public Optional<Long> getCurrentAuditor() {
        // 从session中获取登录人id
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        Long userId = (Long)servletRequestAttributes.getRequest().getSession().getAttribute("userId");
        return Optional.of(userId);
    }
}

ok,然后jpa将会根据IDVersion判断当前操作是更新还是添加数据,添加时会注入当前登录人id到标有@CreateBy注解的转移上,当前时间注入到标有@CreateTime注解分段上;更新时则注入到@LastModifiedBy和对应@LastModifiedDate的基线上。想详细了解的可查看org.springframework.data.jpa.domain.support.AuditingEntityListener原始代码。

常见的坑

  • N + 1问题,当使用@ManyToMany@ManyToOne@OneToMany@OneToOne关联关系的时候,FetchType怎么配置LAZY或者EAGER.SQL执行真正的时候的英文由一条主表查询和Ñ条子表查询组成的。这种查询效率一般比较低下,某些子对象有N个就会执行N + 1条SQL。使用JPA 2.1启动的@ EntityGraph,@ NamedEntityGraph可以解决该问题。如下。


 
   




     

       serialVersionUID = 1L ; @ApiModelProperty (隐藏= 真)@Id @GeneratedValue (策略= GenerationType 。 IDENTITY )私人龙ID ; //省略其他属性} ```

     
    
    
    
    
    



```java
package com.yizhu.repository;

import com.yizhu.entity.User;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {

    /**
     * 根据id查询用户信息
     * @param id
     * @return
     */
    @EntityGraph(value = "User.findUsers", type = EntityGraph.EntityGraphType.FETCH)
    User findAllById(Long id);

    /**
     * 根据name查询用户信息
     * @param name
     * @return
     */
    @EntityGraph(value = "User.findUsers", type = EntityGraph.EntityGraphType.FETCH)
    @Query(value = "select * from t_user where user_name = :name", nativeQuery = true)
    List<User> findAllByUserName(@Param("name") String name);
}
  • 所有的注解要么全配置在字段上,要么全配置在获得方法上,不能混用,混用就会启动不起来,但是语法配置没有问题。- 所有的关联都是支持单向关联和双向关联的,- 在所有的关联查询中,表一般是不需要建立外键索引的。。在特定情况下,视听特定业务场景而定。JSON序列化的时间使用双向注解会产生死循环,需要人为手动转换一次,或者使用@JsonIgnore 。 @mappedBy的使用需要注意。- 级联删除比较危险,建议考虑清楚,或者完全掌握。- 不同的关联关系的配置,@ JoinClumn里面的名字,referencedColumnName代表的意思是不一样的,很容易弄混,可以根据打印出来的SQL做调整。- 当配置这些关联关系的时候建议大家直接在表上面,把外键建好,然后通过后面我们介绍的开发工具直接生成,这样可以减少自己调试的时间。

JPA常用注解

摘自《 Spring Data JPA从入门到精通》

更多信息可以关注我的个人博客:[逸竹小站]

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
0
获赞与收藏
6

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消