您的位置:首页 > 编程语言 > Java开发

以后直接拿着用:SpringBoot+shiro+Swagger实现前后分离的框架

2020-03-06 15:44 555 查看

文章目录

  • 10、controller的编写
  • 11、Service的编写
  • 12、Mapper接口的编写
  • 13、mapper.xml文件的编写
  • 14、用户对象的编写
  • 15、常量类的编写
  • 16、properties文件的编写
  • 17、自定义异常的编写
  • 1、导包

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.54</version>
    </dependency>
    
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
    </dependency>
    
    <!--下面导入数据库的使用的包-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
    </dependency>
    
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    
    <!--使用Druid这个连接池-->
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.0</version>
    </dependency>
    
    <!--下面要导入Swagger2的相关的包-->
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.7.0</version>
    </dependency>
    
    <dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.7.0</version>
    </dependency>

    2、封装token

    public class CustomToken extends UsernamePasswordToken {
    
    private String token;   //用户身份唯一的标识
    //这个token是在认证通过之后  用户访问其他资源的时候 来进行你给身份识别的
    
    public CustomToken(String token){
    this.token=token;
    }
    
    @Override
    public Object getPrincipal() {
    //在用户认证通过之后 再访问这个方法 默认返回的是什么?
    // Realm校验的第一个参数
    //校验我们自己写了   还有没有第一个参数这个说法?
    return token;
    }
    }

    3、编写过滤器

    public class CustomAccessControllerFilter extends AccessControlFilter {
    
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
    return false;
    }
    
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
    HttpServletRequest request= (HttpServletRequest) servletRequest;
    try {
    //校验身份
    //逻辑是什么?
    //第一步:获取token
    String token=request.getHeader(Constant.REQ_TOKEN);
    //第二步:看下这个token是否否为""
    if(StringUtils.isEmpty(token)){  //说明你娃身份是非法的
    throw new BusinessException(400001,"用户的请求的token不能为空");
    }else{  //说明用户带了token
    //逻辑
    //这里要将token进行封装  封装完了 交给shiro去做认证  看下身份是否合法
    CustomToken customToken = new CustomToken(token);
    //记住下面这个类 在用户第一次登陆的时候  并不会执行
    // 这个执行的时候 是在认证成功之后访问其他资源的
    //的时候 机械能给你身份校验的
    getSubject(servletRequest,servletResponse).login(customToken);
    }
    } catch (BusinessException e) {
    //如果是这个异常:返回JSON告诉前端出现问题了
    resultResponse(e.getMessageCode(),e.getDefaultMesaage(),servletResponse);
    return false;
    } catch (AuthenticationException e) {  //校验没通过的异常
    //  e.getCause() :返回的是当前异常的实例
    if(e.getCause() instanceof BusinessException){ //表示返回的是我们自定义的异常
    //将异常的实例进行转换
    BusinessException err= (BusinessException) e.getCause();
    resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
    }else{  //如果执行到这里  说明 这个异常是shiro返回的
    resultResponse(400001,"用户的认证是失败的",servletResponse);
    }
    return false;
    }catch (AuthorizationException e){
    //  e.getCause() :返回的是当前异常的实例
    if(e.getCause() instanceof BusinessException){ //表示返回的是我们自定义的异常
    //将异常的实例进行转换
    BusinessException err= (BusinessException) e.getCause();
    resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
    }else{  //如果执行到这里  说明 这个异常是shiro返回的
    resultResponse(403001,"用户没有访问权限",servletResponse);
    }
    return false;
    }catch (Exception e){  //这个分支就捕获一些未考虑的异常了
    //  e.getCause() :返回的是当前异常的实例
    if(e.getCause() instanceof BusinessException){ //表示返回的是我们自定义的异常
    //将异常的实例进行转换
    BusinessException err= (BusinessException) e.getCause();
    resultResponse(err.getMessageCode(),err.getDefaultMesaage(),servletResponse);
    }else{  //如果执行到这里  说明 这个异常是shiro返回的
    resultResponse(500001,"系统出现了异常",servletResponse);
    }
    return false;
    }
    //当前的方法返回true才放行  否则这个程序也就执行到这里了....
    return true;
    }
    
    /**
    * 这个方法的主要功能就是告诉前端 一些出错的信息
    * @param messageCode
    * @param defaultMesaage
    * @param response
    */
    private void resultResponse(int messageCode, String defaultMesaage, ServletResponse response) {
    
    //构建返回的数据
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("code",messageCode);
    jsonObject.put("msg",defaultMesaage);
    //设置下返回的数据类型
    response.setContentType(MediaType.APPLICATION_JSON_UTF8.toString());
    //获取输出流
    try {
    ServletOutputStream out = response.getOutputStream();
    //接下来项数据写出去
    out.write(jsonObject.toJSONString().getBytes());
    out.flush();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }

    4、编写凭证匹配器

    public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
    
    @Autowired
    private IUserService userService;
    
    /**
    * 下面这个方法 返回true 或者 false就决定了 这个是成功呢还是失败
    * @param token
    * @param info
    * @return
    */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    //  这里面要实现的功能很简单
    //  把前面传递过来的token取出来  再把存储到服务器的token取出来做比较
    // 如果一致那么就返回true  否则就返回  false
    CustomToken token1= (CustomToken) token;
    // 取出 Principal的值 (下面这个值 就是从前端传递过来进行比较的)
    String tokenVal= (String) token1.getPrincipal();
    // 从redis  或者  数据库    或者 session取出这个信息来
    //假设取出来了....
    //String tokenServer="xiaoboboxiaowangzi";
    boolean b=false;
    try{
    b = userService.tokenExistsOrNot(tokenVal);
    }catch (Exception err){
    throw new BusinessException(500001,"查询token存在失败"+err.getMessage());
    }
    //进行比较 判定前端的token和服务端的token是否一致  如果一致  那么返回true  否则返回false
    if(!b){
    throw new BusinessException(4010000,"授权信息无效请重新登录");
    }
    return true;
    }
    }

    5、编写realm

    public class CustomRealm extends AuthorizingRealm {
    /**
    * 授权的方法
    * @param principalCollection
    * @return
    */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    return simpleAuthorizationInfo;
    }
    
    /**
    * 认证的方法
    * 将前端放进去的token 取出来 放到校验的SimpleAuthenticationInfo中去 方便后面进行校验
    * token放到哪里呢?
    * @param authenticationToken
    * @return
    * @throws AuthenticationException
    */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //取出前端传递过来的token
    CustomToken customToken= (CustomToken) authenticationToken;
    String token= (String) customToken.getPrincipal();
    
    //这里就可以取出这个token
    //在这里要将前端传递过来的token给封装到 SimpleAuthenticationInfo 对象中去
    SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(token,token,getName());
    return simpleAuthenticationInfo;
    }
    }

    6、编写Swagger的配置文件

    @SpringBootConfiguration
    @EnableSwagger2
    public class SwaggerConfig {
    
    @Bean
    public Docket createApi(){
    return new Docket(DocumentationType.SWAGGER_2)
    .apiInfo(apiInfo())
    .select()
    .apis(RequestHandlerSelectors.basePackage("com.qf.shiro.controller"))
    .paths(PathSelectors.any())
    .build();
    }
    
    /**
    * 页面信息的配置
    * @return
    */
    private ApiInfo apiInfo(){
    return new ApiInfoBuilder()
    .title("springboot+shiro+swagger测试")
    .description("这里是整合shiro和Swagger实现前后分离")
    .version("v1.0")
    .build();
    }
    }

    7、编写shiro的配置文件

    @SpringBootConfiguration
    public class ShiroConfig {
    
    /**
    * 咋们项目认证(请求资源的时候 身份的认证)的realm
    * @return
    */
    @Bean
    public CustomRealm customRealm(){
    CustomRealm customRealm = new CustomRealm();
    customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
    return customRealm;
    }
    
    /**
    * 凭证匹配器的申明
    * @return
    */
    @Bean
    public CustomHashedCredentialsMatcher customHashedCredentialsMatcher(){
    return new CustomHashedCredentialsMatcher();
    }
    
    /**
    * 安全管理器
    * @return
    */
    @Bean
    public DefaultWebSecurityManager securityManager(){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(customRealm());
    return securityManager;
    }
    
    /**
    * 配置的是目标过滤器的代理
    * @param securityManager
    * @return
    */
    @Bean
    public ShiroFilterFactoryBean  shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    //配置安全管理器
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    //接下来配置些过滤器
    //配置自己的那个过滤器
    Map<String, Filter> maps=new LinkedHashMap<>();
    maps.put("token",new CustomAccessControllerFilter());
    shiroFilterFactoryBean.setFilters(maps);
    
    //对请求过滤和拦截的设置
    Map<String,String> maps1=new LinkedHashMap<>();
    //放入不拦截的页面  拦截的页面....
    maps1.put("/user/login","anon");
    //Swagger的所有请求的资源和请求的地址都不需要拦截
    maps1.put("/swagger/**","anon");
    maps1.put("/v2/api-docs","anon");
    maps1.put("/swagger-ui.html","anon");
    maps1.put("/swagger-resources/**","anon");
    maps1.put("/webjars/**","anon");
    maps1.put("/favicon.ico","anon");
    maps1.put("/captcha.jpg","anon");
    maps1.put("/csrf","anon");
    //设置我们自己的校验
    maps1.put("/**","token,authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(maps1);
    return shiroFilterFactoryBean;
    }
    
    /**
    * 开启aop的注解的支持
    */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultSecurityManager securityManager){
    AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
    attributeSourceAdvisor.setSecurityManager(securityManager);
    return attributeSourceAdvisor;
    }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
    }
    }

    8、编写项目的配置文件

    @SpringBootConfiguration
    @ComponentScan(basePackages = {"com.qf.shiro"})
    @MapperScan(basePackages = {"com.qf.shiro.mapper"})
    public class AppConfig {
    
    }

    9、结果集的封装

    9.1、接口的封装

    public interface ResponseCodeInterface {
    /**
    * 返回的码的一个获取
    * @return
    */
    int getCode();
    
    /**
    * 返回消息的获取
    * @return
    */
    String getMsg();
    }

    9.2、返回信息码值和提示信息的封装

    public enum BaseResponseCode implements ResponseCodeInterface{
    /**
    * 接下来就要和前端约束好所有的码值以及含义
    */
    SUCCESS(0,"操作成功"),
    SYSTEM_ERROR(5000001,"系统错误"),
    METHOD_INVALIDATE(4000001,"数据校验出错"),
    DATA_ERROR(4000002,"传入数据异常"),
    TOKEN_NOT_NULL(4010001,"用户认证异常");
    
    //整个构造函数
    BaseResponseCode(int code,String msg){
    this.code=code;
    this.msg=msg;
    }
    
    private int code;
    private String msg;
    
    @Override
    public int getCode() {
    return code;
    }
    
    @Override
    public String getMsg() {
    return msg;
    }
    }

    9.3、返回数据的封装

    @Data
    public class DataResult <T> {
    private int code;   //返回的码值
    private String msg; //返回的错误信息提示
    private T data;   //返回的数据
    
    //下面这一块是对构造器进行封装
    
    public DataResult(int code,T data){
    this.code=code;
    this.data=data;
    this.msg=null;
    }
    
    public DataResult(int code,String msg,T data){
    this.code=code;
    this.data=data;
    this.msg=msg;
    }
    
    public DataResult(int code,String msg){
    this.code=code;
    this.msg=msg;
    }
    
    public DataResult(){
    this.code=BaseResponseCode.SUCCESS.getCode();
    this.msg=BaseResponseCode.SUCCESS.getMsg();
    this.data=null;
    }
    
    public DataResult(T data){
    this.code=BaseResponseCode.SUCCESS.getCode();
    this.msg=BaseResponseCode.SUCCESS.getMsg();
    this.data=data;
    }
    
    public DataResult(ResponseCodeInterface responseCodeInterface){
    this.data=null;
    this.code=responseCodeInterface.getCode();
    this.msg=responseCodeInterface.getMsg();
    }
    
    public DataResult(ResponseCodeInterface responseCodeInterface,T data){
    this.data=data;
    this.code=responseCodeInterface.getCode();
    this.msg=responseCodeInterface.getMsg();
    }
    
    /**
    * 这个很牛逼
    * 不带数据的返回
    * @param <T>
    * @return
    */
    public static <T>DataResult success(){
    return new DataResult();
    }
    
    /**
    * 带数据的返回
    * @param data
    * @param <T>
    * @return
    */
    public static <T>DataResult success(T data){
    return new DataResult(data);
    }
    
    /**
    * 自己给参数的问题
    * @param code
    * @param msg
    * @param data
    * @param <T>
    * @return
    */
    public static <T>DataResult getResult(int code,String msg,T data){
    return new <T>DataResult(code,msg,data);
    }
    /**
    * 自己给参数的问题
    * @param code
    * @param msg
    * @param <T>
    * @return
    */
    public static <T>DataResult getResult(int code,String msg){
    return new <T>DataResult(code,msg);
    }
    
    /**
    * 直接传递一个枚举进来
    * @param baseResponseCode
    * @param <T>
    * @return
    */
    public static <T>DataResult getResult(BaseResponseCode baseResponseCode){
    return new <T>DataResult(baseResponseCode);
    }
    
    /**
    * 直接传递一个枚举进来
    * @param baseResponseCode
    * @param <T>
    * @return
    */
    public static <T>DataResult getResult(BaseResponseCode baseResponseCode,T data){
    return new <T>DataResult(baseResponseCode,data);
    }
    }

    10、controller的编写

    @RestController
    @Api(tags = {"用户接口"})
    public class UserController {
    
    @Autowired
    private IUserService userService;
    
    private Logger logger= LoggerFactory.getLogger(UserController.class);
    
    /**
    * 登陆的接口
    * @param user
    * @return
    */
    @RequestMapping(value = "/user/login",method = RequestMethod.POST)
    @ApiOperation(value = "用户登陆的接口")
    public DataResult<User> login(@RequestBody User user){
    //这个里面应该干什么?
    /**
    * 说白了 调用业务逻辑层的方法
    *  异常的捕获
    *  返回数据
    */
    DataResult<User> dataResult=null;
    try {
    User user1 = userService.login(user);
    dataResult=DataResult.success(user1);
    } catch (Exception e) {
    if(e instanceof BusinessException){  //说明是业务异常
    BusinessException err= (BusinessException) e;
    //应该干什么?
    dataResult=new DataResult<>(err.getMessageCode(),err.getDefaultMesaage());
    }else{
    //dataResult=new DataResult<>(500001,"系统异常造成登陆失败");
    dataResult=DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(),BaseResponseCode.SYSTEM_ERROR.getMsg());
    }
    return dataResult;
    }
    return dataResult;
    }
    
    /**
    * 查找所有的用户数据
    * @return
    */
    @RequestMapping(value = "/user/list",method = RequestMethod.GET)
    @ApiOperation(value = "获取所有的用户信息")
    @ApiImplicitParam(paramType = "header",name = "token",value = "用户token",required = true,dataType = "String")
    public DataResult<List<User>> findUserList(){
    //定义返回数据
    DataResult<List<User>> userLists;
    try{
    //返回用户数据
    List<User> users = userService.findUserList();
    userLists=DataResult.success(users);
    logger.info("获取数据成功....");
    }catch (Exception err){
    //说明获取信息失败了
    logger.error("获取用户信息失败:"+err.fillInStackTrace());
    userLists=DataResult.getResult(BaseResponseCode.SYSTEM_ERROR.getCode(),BaseResponseCode.SYSTEM_ERROR.getMsg());
    }
    return userLists;
    }
    }

    11、Service的编写

    11.1、Service接口的编写

    public interface IUserService {
    /**
    * 登陆
    * @param user
    * @return
    */
    User login(User user);
    
    /**
    * 通过名字找到用户
    * @param userName
    * @return
    */
    User findUserByName(String userName);
    
    /**
    * 更新token到数据库
    * @param user
    */
    void updateToken(User user);
    
    /**
    * 查询所有的用户
    * @return
    */
    List<User> findUserList()throws Exception;
    
    /**
    * 判定这个token是否存在
    * @param token
    * @return
    */
    boolean tokenExistsOrNot(String token);
    
    }

    11.2、Service实现的编写

    @Service
    @Transactional
    public class UserService implements IUserService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public User login(User user){
    //这个类里面应该干什么?
    /**第一步:获取到前端传递过来的用户名
    *第二步:通过用户名 获取用户对象
    * 第三步:校验
    * 第四步:生成token保存到数据库
    * 第五步:将token封装到返回数据里面给前端
    */
    //获取用户名
    String userName = user.getUserName();
    //通过用户名 找用户名找对象
    User userResult = findUserByName(userName);
    //第三步:校验
    if(null==userResult){  //说明用户名不对
    throw new BusinessException(40001,"用户名不对");
    }
    //说明:用户名是对的
    //比较密码
    if(!(userResult.getPassword().equals(user.getPassword()))){
    throw new BusinessException(40002,"密码不对");
    }
    //执行到这里说明用户身份合法的
    //先将数据保存到一个类里面
    //首先要生成token这个值
    String token= UUID.randomUUID().toString();
    Date date=new Date();
    //设置这个值给user对象
    userResult.setToken(token);
    userResult.setExpireDate(date);
    //下面就是更新这个数据库的数据
    updateToken(userResult);
    //将这个信息返回给前端
    //一般情况下 密码是不需要返回的
    userResult.setPassword("");
    //设置返回数据的对象
    //DataResult<User> userDataResult = new DataResult<>(0,"认证成功",userResult);
    return userResult;
    }
    
    @Override
    public User findUserByName(String userName) {
    return userMapper.findUserByName(userName);
    }
    
    @Override
    public void updateToken(User user) {
    userMapper.updateToken(user);
    }
    
    @Override
    public List<User> findUserList() throws Exception{
    List<User> userList = userMapper.findUserList();
    //接下来对数据进行封装
    DataResult<List<User>> dataResult = new DataResult<>(0, "请求完美", userList);
    return userList;
    }
    
    @Override
    public boolean tokenExistsOrNot(String token) {
    //通过token查询用户信息
    User userResult = userMapper.findUserByToken(token);
    //接下来就要判断了
    if(null!=userResult){
    return true;
    }
    return false;
    }
    }

    12、Mapper接口的编写

    public interface UserMapper {
    
    /**
    * 通过名字找到用户
    * @param userName
    * @return
    */
    User findUserByName(String userName);
    
    /**
    * 更新token到数据库
    * @param user
    */
    void updateToken(User user);
    
    /**
    * 查询所有的用户
    * @return
    */
    List<User> findUserList();
    /**
    * 查看当前的token是否在数据库中存在
    * @param token
    * @return
    */
    User findUserByToken(String token);
    
    }

    13、mapper.xml文件的编写

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!--要注意的第二点-->
    <mapper namespace="com.qf.shiro.mapper.UserMapper">
    
    <!--通过用户名找用户-->
    <select id="findUserByName" parameterType="string" resultType="user">
    select * from t_user where userName=#{value}
    </select>
    
    <!--更新数据库用户的token-->
    <update id="updateToken" parameterType="user">
    update t_user set token=#{token} where id=#{id}
    </update>
    
    <!--查找所有的用户-->
    <select id="findUserList" resultType="user">
    select * from t_user
    </select>
    
    <!--查看token是否存在-->
    <select id="findUserByToken" parameterType="String" resultType="user">
    select * from t_user where token=#{value}
    </select>
    </mapper>

    14、用户对象的编写

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
    private static final long serialVersionUID = -5199670739008077879L;
    private Integer id;
    private String userName;
    private String password;
    private String token;
    private Date expireDate;  //token的过期时间
    }

    15、常量类的编写

    public class Constant {
    public static final String REQ_TOKEN="token";
    }

    16、properties文件的编写

    mybatis.type-aliases-package=com.qf.shiro.pojo
    mybatis.mapper-locations=classpath:mapper/*.xml
    
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql:///NZ1904
    spring.datasource.username=root
    spring.datasource.password=root

    17、自定义异常的编写

    public class BusinessException extends RuntimeException{
    private int messageCode;
    private String defaultMesaage;
    public BusinessException(int messageCode,String defaultMesaage){
    super(defaultMesaage);
    this.messageCode=messageCode;
    this.defaultMesaage=defaultMesaage;
    }
    
    public String getDefaultMesaage() {
    return defaultMesaage;
    }
    
    public int getMessageCode() {
    return messageCode;
    }
    }

    希望大家关注我一波,防止以后迷路,有需要的可以加我Q讨论互相学习java ,学习路线探讨,经验分享与java Q:2415773436

    • 点赞
    • 收藏
    • 分享
    • 文章举报
    yuemmm 发布了17 篇原创文章 · 获赞 0 · 访问量 440 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: