Myblog项目总结

A. 项目介绍

I. 技术栈及工具

  • Maven
  • Spring, SpringMVC, SpringBoot
  • MyBatis Plus
  • Mysql
  • Redis
  • Spring Security
  • docker
  • nginx
  • 七牛云

B. 开发

I. 项目结构

- common:
  - aop
  - cache
- config:
- controller:
- dao:
  - dos
  - mapper
  - pojo
- handler:
- service:
    - impl
- utils:
- vo:
  - params

项目采用经典MVC结构,即View为前端实现、Controller进行控制、Service负责业务逻辑、dao层用于操作数据库。在此之外,我们还加入了以下一些常用packages:

  • common
    common 基础工具包和常量package。比如用aop实现日志输出和缓存类。

  • config
    config是项目中一般都有的包用于存放整个项目的配置类。比如常见的WebMVCConfig类定义跨域ip,拦截器等等、线程池配置类或者ORM框架配置类......

  • handler
    handler也是项目中非常常见的package,存放例如通用的异常处理、登录拦截器等等。

  • utils
    用于存放通用工具类

  • vo

    vo(view object)类用于存放视图对象,与前端交互专用。使用过程通常如下:
    a. 从数据库中提取数据并存为entity类型对象
    b. 用org.springframework.beans.BeanUtils的BeanUtils.copyProperties(article, articleVo);方法把存于pojo对象中的数据复制到Vo对象中(注意:如果变 量名或者变量类型不同时,无法自动复制数据,此时需要手动在service中用set方法复制数据)

    vo中往往包含params package,用于存放前端传来的数据。

另外比较值得关注的是dao层的结构,dao层通常包含以下packages:

  • pojo: 与数据库表结构一一对应,通过DAO层向上传输数据源对象。

  • dos: 与数据库表结构不一致,通过DAO层向上传输临时数据源对象

  • mapper:用于操作数据库表

参考:https://juejin.cn/post/6844903636334542856

II. 项目功能实现

  • 业务功能(博客、评论、标签、分类、账户的增删改查)

    • 雪花算法生成Entity ID

      我们使用雪花算法来分配数据ID用于避免分布式系统数据ID相同的问题,在MyBatis-Plus 3.3版本之后自动实现雪花算法生成ID。

    • MyBatis自动分页

      在MyBatis配置类中添加分页配置即可自动实现分页

      package com.soul.blog.config;
      
      import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
      import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
      import org.mybatis.spring.annotation.MapperScan;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      @Configuration
      @MapperScan("com.soul.blog.dao.mapper") // scan all mapper in this location
      public class MybatisPlusConfig {
      
        /**
         * paging plugin
         * @return
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
          MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
          interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
          return interceptor;
        }
      }
      
  • 登录/登出

    • MD5加密

      为避免用户的账号密码泄漏,我们将存于数据库中的密码使用MD5进行加密

    • JWT生成Token并用Redis储存

      前端将用户的账户密码传入后端并验证成功后,使用JWT技术生成一个特定的Token暂时存于缓存数据库Redis中并返回给前端存于cookie中。这样前端每次请求都携带这个token,用户便可以一直以登录状态访问网站直到token在Redis中被自动删除。(本程序设定为1日后删除)

      以下为JWT工具类模板,定义了生成token和解析token的方法:

      package com.soul.blog.utils;
      
      import io.jsonwebtoken.Jwt;
      import io.jsonwebtoken.JwtBuilder;
      import io.jsonwebtoken.Jwts;
      import io.jsonwebtoken.SignatureAlgorithm;
      import java.util.Date;
      import java.util.HashMap;
      import java.util.Map;
      
      public class JWTUtils {
      
        private static final String jwtToken = "123456Myblog!@#$$";
      
        public static String createToken(Long userId){
          Map<String,Object> claims = new HashMap<>();
          claims.put("userId",userId);
          JwtBuilder jwtBuilder = Jwts.builder()
              .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
              .setClaims(claims) // body数据,要唯一,自行设置
              .setIssuedAt(new Date()) // 设置签发时间
              .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
          String token = jwtBuilder.compact();
          return token;
        }
      
        public static Map<String, Object> checkToken(String token){
          try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
          }catch (Exception e){
            e.printStackTrace();
          }
          return null;
      
        }
      }
      
- **登录拦截器**

    在用户还未登录的情况下,我们不希望有些功能向他们开发,这时候我们就可以使用登录拦截器来定义哪些地址无法被未登录的用户使用。为实现此功能,我们首先需要定义一个LoginInterceptor,并在`WebMVCContig implements WebMvcConfigurer`下的`addInterceptors`方法中定义需要拦截的地址:

    ```java
    package com.soul.blog.handler;
    
    import com.alibaba.fastjson.JSON;
    import com.soul.blog.dao.pojo.SysUser;
    import com.soul.blog.service.LoginService;
    import com.soul.blog.utils.UserThreadLocal;
    import com.soul.blog.vo.ErrorCode;
    import com.soul.blog.vo.Result;
    import java.io.IOException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import lombok.extern.log4j.Log4j2;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    @Component
    @Log4j2
    public class LoginInterceptor implements HandlerInterceptor {
    
      @Autowired
      private LoginService loginService;
    
      /**
       * 在执行controller方法(handler)之前进行执行
       * @param request
       * @param response
       * @param handler
       * @return
       * @throws Exception
       */
      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
          throws Exception {
        /**
         * 1. 需要判断 请求的接口路径 是否为HandlerMethod(controller方法)
         * 2. 判断token是否为空,如果为空-> 未登录
         * 3. 如果token 不为空-> 登录验证 loginService checkToken
         * 4. 如果认证成功-> 放行即可
         */
        if (!(handler instanceof HandlerMethod)) {
          // handler 可能是 RequestResourceHandler。springboot程序访问静态资源时,默认去classpath下的static目录去查询
          return true;
        }
    
        String token = request.getHeader("Authorization");
    
        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");
    
    
        if (StringUtils.isBlank(token)) {
          printNoLogin(response);
          return false;
        }
    
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null) { // if no this token in redis
          printNoLogin(response);
          return false;
        }
        // 我们希望在Controller中 直接获取用户的信息 -> 使用TreadLocal
        UserThreadLocal.put(sysUser);
        return true;  // 放行
      }
    
      @Override
      public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
          Object handler, Exception ex) throws Exception {
        // 如果不删除ThreadLocal中用完的信息,会有内存泄漏的风险
        UserThreadLocal.remove();
      }
    
      private void printNoLogin(HttpServletResponse response) throws IOException {
        Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().print(JSON.toJSONString(result));  // 把result写入response
      }
    
    
    }
    
    ```

    

    ```java
      @Autowired
      private LoginInterceptor loginInterceptor;
    
      /**
       * inject interceptor to SpringMVC
       */
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
            .addPathPatterns("/test")
            .addPathPatterns("/comments/create/change") // 拦截test接口
            .addPathPatterns("/articles/publish")
            ;
    
      }
    ```

III. 项目优化

  • 跨域

    通常情况下,我们的Request来自另一个端口或IP,这时候我们就需要在WebMVCContig implements WebMvcConfigurer下的addCorsMappings方法中定义允许访问程序的IP地址和端口号:

      /**
       * Cors (跨域) conf.
       * @param registry
       */
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*");
    //    registry.addMapping("/**").allowedOrigins("http://85.214.77.110:81");
    //    registry.addMapping("/**").allowedOrigins("http://localhost:81");
      }
    
  • 缓存数据

    我们可以将用户访问过的文章加入到Redis中,这样用户再次访问它时可以直接走缓存加载,这样提高了程序的性能。实现缓存的步骤如下:

      a. 在common包中定义缓存注解:
    
    package com.soul.blog.common.cache;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Cache {
    
      long expire() default 1 * 60 * 1000;
    
      String name() default "";
    }
    
    
    package com.soul.blog.common.cache;
    
    import com.alibaba.fastjson.JSON;
    import com.soul.blog.vo.Result;
    import java.lang.reflect.Method;
    import java.time.Duration;
    import lombok.extern.log4j.Log4j2;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.Signature;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    
    @Aspect
    @Component
    @Log4j2
    public class CacheAspect {
    
      @Autowired
      private RedisTemplate<String, String> redisTemplate;
    
      @Pointcut("@annotation(com.soul.blog.common.cache.Cache)")
      public void pt(){}
    
      @Around("pt()")
      public Object around(ProceedingJoinPoint pjp) {
        try{
          Signature signature = pjp.getSignature();
          // 类名
          String className = pjp.getTarget().getClass().getSimpleName();
          // 调用的方法名
          String methodName = signature.getName();
    
          Class[] parameterTypes = new Class[pjp.getArgs().length];
          Object[] args = pjp.getArgs();
          // 参数
          String params = "";
          for (int i = 0; i < args.length; i++) {
            if (args[i] != null) {
              params += JSON.toJSONString(args[i]);
              parameterTypes[i] = args[i].getClass();
            } else {
              parameterTypes[i] = null;
            }
          }
          if (StringUtils.isNotEmpty(params)) {
            // 加密 以防出现key过长以及字符转义获取不到的情况
            params = DigestUtils.md5Hex(params);
          }
          Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
          //获取Cache 注解
          Cache annotation = method.getAnnotation(Cache.class);
          long expire = annotation.expire();
          String name = annotation.name();
          // 先从redis获取
          String redisKey = name + "::" + className + "::" + methodName + "::" + params;
          String redisValue = redisTemplate.opsForValue().get(redisKey);
          if (StringUtils.isNotEmpty(redisValue)) {
            log.info("走了缓存~~~, {}, {}", className, methodName);
            Result result = JSON.parseObject(redisValue, Result.class);
            return result;
          }
          Object proceed = pjp.proceed(); // 执行原方法
          redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(proceed), Duration.ofMillis(expire));
          log.info("存入缓存~~~ {}, {}", className, methodName);
          return proceed;
        } catch (Throwable throwable) {
          throwable.printStackTrace();
        }
    
        return Result.fail(-999, "系统错误");
      }
    }
    
    
      b. 在需要使用的方法上添加`@Cache`注解
    
      /**
       * return articles to the index page.
       * @param pageParams
       * @return
       */
      @PostMapping
      //加上此注解 代表要对此接口记录日志
      @LogAnnotation(module="文章", operator="获取文章列表")
      @Cache(expire = 5 * 60 * 1000,name = "listArticle")   // 缓存注解
      public Result listArticle(@RequestBody PageParams pageParams) {
        return articleService.listArticle(pageParams);
      }
    
  • 日志输出

    我们可以使用Spring Boot的AOP功能添加日志输出,步骤如下:

      a. 编写日志注解
    
    package com.mszlu.blog.common.aop;
    
    import java.lang.annotation.*;
    //Type 代表可以放在类上面 Method 代表可以放在方法上
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface LogAnnotation {
    
        String module() default "";
    
        String operator() default "";
    }
    
    
    package com.mszlu.blog.common.aop;
    
    import com.alibaba.fastjson.JSON;
    import com.mszlu.blog.utils.HttpContextUtils;
    import com.mszlu.blog.utils.IpUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    
    @Component
    @Aspect //切面 定义了通知和切点的关系
    @Slf4j
    public class LogAspect {
    
        @Pointcut("@annotation(com.mszlu.blog.common.aop.LogAnnotation)")
        public void pt(){}
    
        //环绕通知
        @Around("pt()")
        public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
            long beginTime = System.currentTimeMillis();
            //执行方法
            Object result = joinPoint.proceed();
            //执行时长(毫秒)
            long time = System.currentTimeMillis() - beginTime;
            //保存日志
            recordLog(joinPoint, time);
            return result;
        }
    
        private void recordLog(ProceedingJoinPoint joinPoint, long time) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
            log.info("=====================log start================================");
            log.info("module:{}",logAnnotation.module());
            log.info("operation:{}",logAnnotation.operator());
    
            //请求的方法名
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();
            log.info("request method:{}",className + "." + methodName + "()");
    
    //        //请求的参数
            Object[] args = joinPoint.getArgs();
            String params = JSON.toJSONString(args[0]);
            log.info("params:{}",params);
    
            //获取request 设置IP地址
            HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
            log.info("ip:{}", IpUtils.getIpAddr(request));
    
    
            log.info("excute time : {} ms",time);
            log.info("=====================log end================================");
        }
    }
    
    b. 为需要日志输出功能的方法添加此注解

```java

  @PostMapping
  //加上此注解 代表要对此接口记录日志
  @LogAnnotation(module="文章", operator="获取文章列表")
  @Cache(expire = 5 * 60 * 1000,name = "listArticle")
  public Result listArticle(@RequestBody PageParams pageParams) {
    return articleService.listArticle(pageParams);
  }
```
  • 线程池异步操作

    待更新

  • application.yml分类

    一般来说,一个项目我们需要不同的运行环境,这个时候我们就可以定义不同的application.yml来实现定义多运行环境。一般来说,我们可以将此配置放到resources/config文件夹下。定义配置文件步骤如下:

    a. 创建application.yml

    # 主配置
    spring:
      profiles:
        active: prod # 选择使用哪个开发环境(这里使用application-prod.yml)
    

    b. 创建并实现不同的application.yml,一般包括:

    • application-dev.yml
    • application-prod.yml
    • application-test.yml

    c. 可以使用如下命令执行jar文件并指定环境:

    java -jar javafile.jar --spring.profiles.active=prod
    

IV. 小细节

  1. 最好在Entity类中添加Lombok的@NoArgsConstructor@llArgsConstructor注解。否则在使用ORM从数据库中提取数据时,当返回值不完全对应entity中所有的field时,可能会出错。

C. 运维

I. Redis

  1. 安装Redis步骤

    docker pull redis
    
    docker run —name myblog-redis -d -it redis
    
  2. 查看Redis是否被安装且启动

docker exec -it myblog-redis bash # 进入redis container
redis-server --version # 查看redis版本
redis-cli # 进入redis
keys * # 查看数据库缓存
Ctirl + C # 退出Redis
exit 或 Ctirl + P + Q # 退出Docker容器但不关闭
  1. docker-compose.yml编写
  myblog-redis:
    image: redis:6.2.6
    container_name: myblog-redis
    expose:
      - 6379
    networks:
      - myblog

II. Mysql

  1. 安装Mysql和数据卷挂载

    docker run -id -p 3307:3306 --name=c_mysql -v /mnt/docker/mysql/conf:/etc/mysql/conf.d -v /mnt/docker/mysql/logs:/logs -v /mnt/docker/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root mysql
    
  2. 数据初始化

    FROM mysql:8.0.27
    #将初始化数据库命令放入这个文件夹后在初始化后自动执行query
    COPY initial_db.sql /docker-entrypoint-initdb.d/
    
  3. 错误排查

    a. 如果提示缺少mysql-files文件,我们可以自己创建一个mysql-files文件夹并挂载到docker中

  4. docker-compose.yml 编写:

      myblog-mysql:
        build:
          context: ./blog_mysql
        image: mysql
        container_name: myblog-mysql
        ports:
          - 3307:3306
        volumes:
    #      - ./blog_mysql/my.cnf:/etc/mysql/my.cnf
          - ./blog_mysql/data:/var/lib/mysql
          - ./blog_mysql/mysql-files:/var/lib/mysql-files
        environment:
          MYSQL_ROOT_PASSWORD: root
        security_opt: # 不再会有不被允许的log
          - seccomp:unconfined
    #    network_mode: "bridge"
        networks:
          - myblog
    

III. Nginx

1. 编写myblog.conf文件并放入conf.d文件夹中
# 定义http://myblogbackend,并设置负载均衡,weight代表负载均衡权重
upstream myblogbackend {
  server blog-api:8888 weight=1;
}

server {
    # 设置监听端口号和服务器ip
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    # 所有 /api请求会重定向到http://myblogbackend
    location /api {
    proxy_pass http://myblogbackend;
    }

    # 所有 除/api请求会重定向到index.html
    location / {
        root   /myblog/web/;
        index  index.html;
    }

    location ~* \.(jpg|jpeg|gif|png|swf|rar|zip|css|js|map|svg|woff|ttf|txt)$ {
    root /myblog/web/;
    index index.html;
    add_header Access-Control-Allow-Origin *;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}


  1. 编写docker-compose.yml文件
  myblog-nginx:
    image: nginx:1.21.5
    container_name: myblog-nginx
    ports:
      - 81:80
      - 443:443
    links:
      - blog-api
    depends_on:
      - blog-api
    volumes:
      - ./nginx/:/etc/nginx/
      - ./blog-api/web:/myblog/web
    networks:
      - myblog

VI. vue 打包

  1. 我们可以使用如下命令打包vue项目生成一个包含静态文件的文件夹dist在根目录下:
npm run build
  1. 将文件夹复制到nginx中并写好配置即可使用(可以使用docker的数据卷挂载来完成,配置见nginx下的myblog.conf)

V. 完整的docker-compose.yml文件:

version: "3.5"
services:
  myblog-mysql:
    build:
      context: ./blog_mysql
    image: mysql
    container_name: myblog-mysql
    ports:
      - 3307:3306
    volumes:
#      - ./blog_mysql/my.cnf:/etc/mysql/my.cnf
      - ./blog_mysql/data:/var/lib/mysql
      - ./blog_mysql/mysql-files:/var/lib/mysql-files
    environment:
      MYSQL_ROOT_PASSWORD: root
    security_opt: # 不再会有不被允许的log
      - seccomp:unconfined
#    network_mode: "bridge"
    networks:
      - myblog


  myblog-redis:
    image: redis:6.2.6
    container_name: myblog-redis
    expose:
      - 6379
    networks:
      - myblog


  blog-api:
    image: blog-api
    container_name: blog-api
    build:
      context: ./blog-api
    ports:
      - 8887:8888
#    network_mode: "bridge"
    depends_on:
      - myblog-mysql
      - myblog-redis
    links:
      - myblog-mysql
      - myblog-redis
    networks:
      - myblog


  myblog-nginx:
    image: nginx:1.21.5
    container_name: myblog-nginx
    ports:
      - 81:80
      - 443:443
    links:
      - blog-api
    depends_on:
      - blog-api
    volumes:
      - ./nginx/:/etc/nginx/
      - ./blog-api/web:/myblog/web
    networks:
      - myblog


networks:
  myblog:
    name: myblog_network
    driver: bridge
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容