Tensquare笔记

Tensquare笔记

Docker

  1. docker run ‐di ‐‐name=tensquare_mysql ‐p 3306:3306 ‐e
  2. docker run ‐di ‐‐name=tensquare_redis ‐p 6379:6379 redis
  3. docker run ‐di ‐‐name=tensquare_mongo ‐p 27017:27017 mongo
  4. docker run ‐di ‐‐name=tensquare_elasticsearch ‐p 9200:9200 ‐p 9300:9300 elasticsearch:5.6.8
  5. docker run ‐di ‐‐name=tensquare_rabbitmq ‐p 5671:5617 ‐p 5672:5672 ‐p 4369:4369 ‐p 15671:15671 ‐p 15672:15672 ‐p 25672:25672 rabbitmq:management

SpringBoot

tensquare_parent

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

  • 删除父工程不需要的资源
    在这里插入图片描述
  • 修改pom.xml,配置依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tensquare</groupId>
    <artifactId>tensquare_parent</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>
  • 依赖导入
    在这里插入图片描述

搭建公共子模块

  • 公共子模块 tensquare_common
    • 右键点击工程,弹出菜单选择 New -Module 弹出窗口选择Maven - next - finish
      在这里插入图片描述
    • 新建entity包,包下创建类Result,用于控制器类返回结果
      在这里插入图片描述
    • 创建类PageResult ,用于返回分页结果
      在这里插入图片描述
    • 状态码实体类
      在这里插入图片描述
    • 分布式ID生成器

由于我们的数据库在生产环境中要分片部署(MyCat),所以我们不能使用数据库本身的自增功能来产生主键值,只能由程序来生成唯一的主键值。我们采用的是开源的 twitter( 非官方中文惯称:推特.是国外的一个网站,是一个社交网络及微博客服务) 的 snowflake (雪花)算法。
默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以 支持1024台机器,序列号支持1毫秒产生4096个自增序列id . SnowFlake的优点是,整 体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID 作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右

在这里插入图片描述
tensquare_common工程创建util包,将IdWorker.java直接拷贝到tensquare_common工程的util包中。
在这里插入图片描述

基础微服务-标签CRUD

  • 搭建基础微服务模块tensquare_base
    在这里插入图片描述
  • pom.xml引入依赖
	<dependencies>
        <!-- 导入spring data jpa的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- 导入common依赖 -->
        <dependency>
            <groupId>com.tensquare</groupId>
            <artifactId>tensquare_common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
  • 创建启动类
    com.tensquare.recruit.BaseApplication.java
/**
 * 启动类
 */
@SpringBootApplication
public class BaseApplication {
    public static void main(String[] args) {
        SpringApplication.run(BaseApplication.class);
    }

    @Bean
    public IdWorker idWorker() {
        return new IdWorker(1, 1);
    }
}
  • 在resources下创建application.yml
server:
  port:9001
spring:
  application:
    name: tensquare-base #指定服务名
  datasource:
    driveClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.184.134:3306/tensquare_base? characterEncoding=utf‐8
    username: root
    password: 123456
  jpa:
    database: MySQL
    show-sql: true
    generate-ddl: true
  • CRUD实现

    • 在com.tensquare.base包下

      • 创建pojo包 ,包下创建Label实体类
      • 创建dao包,包下创建LabelDao接口

        JpaRepository提供了基本的增删改查
        JpaSpecificationExecutor用于做复杂的条件查询

    • tb_label表接口分析

字段名称 字段含义 字段类型 备注
id ID 文本
labelname 标签名称 文本
state 状态 文本 0:无效 1:有效
count 使用数量 整型
fans 关注数 整型
recommend 是否推荐 文本 0:不推荐 1:推荐
  • 业务逻辑类
    • 创建service包,包下创建LabelService类,实现基本的增删改查功能
      1. 装配LabelDao
      2. 装配IdWorker
      3. 实现crud
  • 控制器类
    • 创建controller包,创建UserController
      1. 设置路由映射
      2. 装配LabelService
      3. 映射GET|POST接口
  • 接口测试
1. GET方法 查询全部数据
	http://localhost:9090/label
2. GET方法 根据ID查询标签
	http://localhost:9090/label/1
3. POST方法 增加数据
	request
		{
			"labelname":"前端",
			"state":"1",
			"count":10,
			"fans":4
		}
	response
		{
			"flag":true,
			"code":20000,
			"message":"增加成功",
			"data":null
		}
4. PUT方法 修改数据
	request
		{
			"labelname":"JAVAEE",
			"state":"1",
			"count":10,
			"fans":4
		}
	response
		{
			"flag":true,
			"code":20000,
			"message":"修改成功",
			"data":null
		}
5. DELETE方法 删除数据
	http://localhost:9001/label/1002447157418135552
	response
		{
			"flag":true,
			"code":20000,
			"message":"删除成功",
			"data":null
		}
  • 异常处理

在com.tensquare.base.controller包下创建公共异常处理类BaseExceptionHandler.java

/**
 * 统一处理异常
 */
@ControllerAdvice
public class BaseExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result error(Exception e){
        e.printStackTrace();
        return new Result(false, StatusCode.ERROR,e.getMessage());
    }
}
  • 跨域处理

    • 跨域是什么?浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、 协议任一不同,都是跨域 。我们是采用前后端分离开发的,也是前后端分离部署的,必 然会存在跨域问题。 怎么解决跨域?很简单,只需要在controller类上添加注解 @CrossOrigin 即可!这个注解其实是CORS的实现。
    • CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思 想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成 功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前 端也需要做一些配置和分析。
  • 标签-条件查询

根据条件查询城市列表 POST /label/search

- Specification条件查询,修改LabelService.java,增加方法
/**
     * 构建查询条件
     *
     * @param searchMap
     * @return
     */
    private Specification<Label> createSpecification(Map searchMap) {
        return new Specification<Label>() {
            @Override
            public Predicate toPredicate(Root<Label> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                List<Predicate> predicateList = new ArrayList<>();
                if (!StringUtils.isEmpty(searchMap.get("labelname"))) {
                    Predicate labelname = criteriaBuilder.like(
                            root.get("labelname").as(String.class),
                            "%" + searchMap.get("labelname") + "%"
                    );
                    predicateList.add(labelname);
                }

                if (!StringUtils.isEmpty(searchMap.get("state"))) {
                    Predicate state = criteriaBuilder.equal(
                            root.get("state").as(String.class),
                            searchMap.get("state")
                    );
                    predicateList.add(state);
                }

                if (!StringUtils.isEmpty(searchMap.get("recommend"))) {
                    Predicate recommend = criteriaBuilder.equal(
                            root.get("recommend").as(String.class),
                            searchMap.get("recommend")
                    );
                    predicateList.add(recommend);
                }
                Predicate[] array = new Predicate[predicateList.size()];
                array = predicateList.toArray(array);
                return criteriaBuilder.and(array);
            }
        };
    }

    /**
     * 条件查询
     * 使用构建的 createSpecification
     *
     * @param searchMap
     * @return
     */
    public List<Label> findSearch(Map searchMap) {
        Specification<Label> specification = createSpecification(searchMap);
        return labelDao.findAll(specification);
    }
- 修改LabelController,增加方法
	/**
     * 根据条件查询
     *
     * @param searchMap
     * @return
     */
    @RequestMapping(value = "/search", method = RequestMethod.POST)
    public Result<List> findSearch(@RequestBody Map searchMap) {
        return new Result<>(true, StatusCode.OK, "查询成功", labelService.findSearch(searchMap));
    }
  • 带分页的条件查询
    • 修改LabelService,增加方法
	/**
     * 分页条件查询
     *
     * @param searchMap
     * @param page
     * @param size
     * @return
     */
    public Page<Label> findSearch(Map searchMap, int page, int size) {
        Specification<Label> specification = createSpecification(searchMap);
        PageRequest pageRequest = PageRequest.of(page - 1, size);
        return labelDao.findAll(specification, pageRequest);
    }
- 修改LabelController,增加方法
	/**
     * 根据条件查询
     *
     * @param searchMap
     * @return
     */
    @RequestMapping(value = "/search", method = RequestMethod.POST)
    public Result<List> findSearch(@RequestBody Map searchMap) {
        return new Result<>(true, StatusCode.OK, "查询成功", labelService.findSearch(searchMap));
    }

    @RequestMapping(value = "/search/{page}/{size}", method = RequestMethod.POST)
    public Result<List> findSearch(@RequestBody Map searchMap, @PathVariable int page, @PathVariable int size) {
        Page<Label> searchPage = labelService.findSearch(searchMap, page, size);
        return new Result(true, StatusCode.OK, "查询成功",
                new PageResult<>(searchPage.getTotalElements(), searchPage.getContent()));
    }

招聘微服务开发

企业信息

  • 企业信息tb_enterprise表结构分析
字段名称 字段含义 字段类型 备注
id ID 文本
name 企业名称 文本
summary 企业简介 文本
address 企业地址 文本
labels 标签列表 文本 用逗号分隔
coordinate 企业位置座标 文本 经度,纬度
ishot 是否热门 文本 0:非热门 1:热门
logo LOGO 文本
jobcount 职位数 数字
url URL 文本

招聘信息

  • 招聘信息tb_recruit表结构分析
字段名称 字段含义 字段类型 备注
id ID 文本
jobname 招聘职位 文本
salary 薪资范围 文本
condition 经验要求 文本
education 学历要求 文本
type 任职方式 文本
address 办公地址 文本
eid 企业ID 文本
createtime 发布日期 日期
state 状态 文本 0:关闭 1:开启 2:推荐
url 原网址 文本
label 标签 文本
content1 职位描述 文本
content2 职位要求 文本
  • 创建 tensquare_recruit 模块,配置pom.xml,参考tensquare_base模块
    • 创建 RecruitApplication.java
    • 配置 application.yml
    • 创建 pojo
      • Enterprise.java
      • Recruit.java
    • 创建 dao
      • EnterpriseDao.java
      • RecruitDao.java
    • 创建 service
      • EnterpriseService.java
      • RecruitService.java
    • 创建controller
      • BaseExceptionHandler.java
      • EnterpriseController.java
      • RecruitController.java

问答微服务开发

  • 相关表
    • tb_problem表
    • tb_reply表
  • 创建 tensquare_qa 微服务模块,配置pom.xml
    • 创建 QaApplication.java
    • 配置 application.yml
  • 创建 pojo
    • Problem.java
    • Reply.java
  • 创建 dao
    • ProblemDao.java
    • ReplyDao.java
  • 创建 service
    • ProblemService.java
    • ReplyService.java
  • 创建 controller
    • BaseExceptionHandler.java
    • ProblemController.java
    • ReplyController.java

文章微服务开发

  • 相关表
    • tb_article表
  • 创建 tensquare_article 微服务模块,配置pom.xml
    • 创建 ArticleApplication.java
    • 配置 application.yml
  • 创建 pojo
    • Article.java
    • Channel.java
    • Column.java
  • 创建 dao
    • ArticleDao.java
    • ChannelDao.java
    • ColumnDao.java
  • 创建 service
    • ArticleService.java
    • ChannelService.java
    • ColumnService.java
  • 创建 controller
    • BaseExceptionHandler.java
    • ArticleController.java
    • ChannelController.java
    • ColumnController.java

活动详情微服务开发

  • 缓存实现

    • Spring Cache使用方法与Spring对事务管理的配置相似。
    • Spring Cache的核心就是对某 个方法进行缓存,其实质就是缓存该方法的返回结果,并把方法参数和结果用键值对的 方式存放到缓存中,当再次调用该方法使用相应的参数时,就会直接从缓存里面取出指 定的结果进行返回。
    • 常用注解:
      • @Cacheable-------使用这个注解的方法在执行后会缓存其返回结果。
      • @CacheEvict--------使用这个注解的方法在其执行前或执行后移除Spring Cache中的某些
        元素。
  • 在SpringBoot中使用SpringCache可以由自动配置功能来完成CacheManager的注册,SpringBoot会自动发现项目中拥有的缓存系统,并注册对应的缓存管理器。当然我们也可以手动指定。
    CacheManager

  • 创建 tensquare_gathering 微服务模块,配置pom.xml

    • 创建 GatheringApplication
      • 为GatheringApplication添加@EnableCaching开启缓存支持
    • 配置 application
  • 创建 pojo

    • Gathering
  • 创建 dao

    • GatheringDao
  • 创建 service

    • GatheringService
  • 创建 controller

    • BaseExceptionHandler
    • GatheringController

吐槽微服务开发

  • 创建 tensquare_spit 微服务模块,配置pom.xml
  • 采用SpringDataMongoDB框架实现吐槽微服务的持久层

文章评论功能开发

  • 评论表 comment
  • 修改tensquare_article工程的pom.xml

搜索微服务开发

  1. 分布式搜索引擎ElasticSearch
    • Elasticsearch是一个实时的分布式搜索和分析引擎。
    • ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分 布式多用户能力的全文搜索引擎,基于RESTful web接口。
    • Elasticsearch是用Java开发 的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。
    • 设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。
  2. ElasticSearch - Head插件的安装与使用
    Head插件图形化界面来实现Elasticsearch的日常管理
  3. ElasticSearch - IK分词器
  4. ElasticSearch - IK分词器 - 自定义词库
  5. 创建模块tensquare_search ,pom.xml引入依赖
  6. 创建com.tensquare.search.dao包,包下建立接口
/**
* 文章数据访问层接口 
*/
 public interface ArticleSearchDao extends 
 		ElasticsearchRepository<Article,String> {}
  1. 同步elasticsearch与MySQL数据 - Logstash
    • Logstash是一款轻量级的日志搜集处理框架,可以方便的把分散的、多样化的日志搜集
      起来,并进行自定义的处理,然后传输到指定的位置,比如某个服务器或者文件。

用户微服务-用户注册

  • 创建tensquare_user模块,配置pom.xml

  • 消息中间件RabbitMQ

    • 消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量 削锋等问题实现高性能,高可用,可伸缩和最终一致性[架构]
    • 使用较多的消息队列有 ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ
    • RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。
    • AMQP :Advanced Message Queue,高级消息队列协议。它是应用层协议的一个开放 标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不 受产品、开发语言等条件的限制。
    • RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展 性、高可用性等方面表现。
      1. 可靠性(Reliability)
      2. 灵活的路由(Flexible Routing)
      3. 消息集群(Clustering)
      4. 高可用(Highly Available Queues)
      5. 多种协议(Multi-protocol)
      6. 多语言客户端(Many Clients)
      7. 管理界面(Management UI)
      8. 跟踪机制(Tracing)
      9. 插件机制(Plugin System)
      10. 主要概念
        在这里插入图片描述
      • RabbitMQ Server: 也叫broker server,它是一种传输服务
      • Producer: 消息生产者,如图A、B、C,数据的发送方。
      • Consumer:消息消费者,如图1、2、3,数据的接收方。
      • Exchange:生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个 或多个Queue中(或者丢弃)。
      • Queue:(队列)是RabbitMQ的内部对象,用于存储消息。
      • RoutingKey:生产者在将消息发送给Exchange的时候,一般会指定一个routing key, 来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联 合使用才能最终生效。
      • Connection: (连接):Producer和Consumer都是通过TCP连接到RabbitMQ Server 的。
      • Channels: (信道):它建立在上述的TCP连接中。数据流动都是在Channel中进行 的。
      • VirtualHost:权限控制的基本单位,一个VirtualHost里面有若干Exchange和 MessageQueue,以及指定被哪些user使用
  • 发送短信验证码

    • 在用户微服务编写API ,生成手机验证码,存入Redis并发送到RabbitMQ

短信微服务

  • 创建tensquare_sms模块,配置pom.xml
  • 消息监听类
  • 短信工具类SmsUtil

密码加密与微服务鉴权JWT

  • BCrypt密码加密,BCrypt强哈希方法,每次加密的结果都不一样。
  • 添加了spring security依赖后,所有的地址都被spring security所控制了,我们目前只是需要用到BCrypt密码加密的部分,所以我们要添加一个配置类,配置为所有地址 都可以匿名访问。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 所有security全注解配置实现的开端,表示开始说明需要的权限,
        // 需要的权限分两部分,
        // 第一部分是拦截的路径,
        // 第二部分访问该路径需要的权限
        http.authorizeRequests()
                // antMatchers表示拦截什么路径,
                .antMatchers("/**")
                //permitAll表示任何权限都可以访问
                .permitAll()
                // anyRequest任何请求,
                .anyRequest()
                //authenticated认证后才能访问
                .authenticated()
                // .and().csrf().disable();固定写法,使得csrf拦截失效
                .and().csrf().disable();
    }
}
  • 管理员密码加密
  • 用户密码加密

常见的认证机制

  1. HTTP Basic Auth 暴露用户信息,避免使用

  2. Cookie Auth 通过客户端带上来Cookie对象来与服务器端的 session对象匹配来实现状态管理的
    在这里插入图片描述

  3. OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提 供者的数据。
    在这里插入图片描述

  4. Token Auth

    1. 客户端使用用户名跟密码请求登录

    2. 服务端收到请求,去验证用户名与密码

    3. 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端

    4. 客户端收到Token以后可以把它存储起来,比如放在Cookie里

    5. 客户端每次向服务端请求资源的时候需要带着服务端签发的Token

    6. 服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据
      在这里插入图片描述

    7. Token机制相对于Cookie机制又有什么好处呢?

      • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提 是传输的用户认证信息通过HTTP头传输.
      • 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为 Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储 状态信息.
      • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript, HTML,图片等),而你的服务端只要提供API即可.
      • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在 你的API被调用的时候,你可以进行Token生成调用即可.
      • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等) 时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认 证机制就会简单得多。 CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防 范。
      • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256 计算 的Token验证和解析要费时得多.
      • 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要 为登录页面做特殊处理.
      • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在 多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如: Firebase,Google, Microsoft).

基于JWT的Token认证机制实现

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用 户和服务器之间传递安全可靠的信息。由三部分组成,头部、载荷与签名

  • 签证(signature)

    • header (base64后的)
    • payload (base64后的)
    • secret

    注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用 来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流 露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

  • 约定

    • 前后端约定:前端请求微服务时需要添加头信息Authorization ,内容为Bearer+空格 +token
  • 使用拦截器方式实现token鉴权

    分别实现预处理,在preHandle中,可以进行编码、安全控制等处理;
    后处理(调用了Service并返回ModelAndView,但未进行页面渲染),在postHandle中,有机会修改ModelAndView;
    返回处理(已经渲染了页面),在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。

    • 配置拦截器类,创建com.tensquare.user.ApplicationConfig
    • 用户登陆签发 JWT
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章