微服务-页面静态化方案2020-03-02

1.概述

页面静态化:在前端门户网站或者其他的一些页面,比如首页,是需要频繁的访问,这样会对服务器造成很大的压力,使用页面静态化,对页面进行一种类似“缓存”的效果,在页面发生改变的时候(添加、修改、删除数据)的时候,对页面进行静态化,输出到静态服务器ngnix,以后访问该页面,直接访问静态化后的页面,减轻服务器的压力!

页面静态化所适用的场景:

①:页面并发量很高

②:数据不易轻易改变

页面静态化是利用模板加数据生成文件,在微服务中,每个服务都是一台电脑,可以分布在不同的地方,那么数据如何传递,模板文件从哪里获得?

页面静态化应该作为一个独立的微服务,需要做页面静态化的服务直接通过feign来调用即可!

2.方案

2020年2月26日-页面静态化-以前画的.png

1:需要一个后台管理中心,管理需要作页面静态化的页面

1583122890173.png

需要保存的数据,页面名字,最终输出到站点的路径、以及模板文件上传到fastdfs的路径!

2:后台管理页面模板文件上传,页面上传,不需要通过feign调用!

①:fastdfs提供上传方法

    @PostMapping("/upload")
    public AjaxResult upload(MultipartFile file){

        String fileExtensionName = FilenameUtils.getExtension(file.getOriginalFilename());
        try {
            String filePath = FastDfsApiOpr.upload(file.getBytes(), fileExtensionName);
            return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
        } catch (IOException e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
        }
    }

②:所用到的工具类

package com.hanfengyi.fastdfs.utils;

import org.csource.fastdfs.*;

public class FastDfsApiOpr {
     
    public static String CONF_FILENAME  = FastDfsApiOpr.class.getClassLoader()
            .getResource("fdfs_client.conf").getFile();


    /**
     * 上传文件
     * @param file
     * @param extName
     * @return
     */
    public static  String upload(byte[] file,String extName) {

        try {
            ClientGlobal.init(CONF_FILENAME);

            TrackerClient tracker = new TrackerClient();
            TrackerServer trackerServer = tracker.getTrackerServer();
            StorageServer storageServer = null;
            StorageClient storageClient = new StorageClient(trackerServer, storageServer);
            String fileIds[] = storageClient.upload_file(file,extName,null);

            System.out.println(fileIds.length);
            System.out.println("组名:" + fileIds[0]);
            System.out.println("路径: " + fileIds[1]);
            return  "/"+fileIds[0]+"/"+fileIds[1];

        } catch (Exception e) {
            e.printStackTrace();
            return  null;
        }
    }
    /**
     * 上传文件
     * @param extName
     * @return
     */
    public static  String upload(String path,String extName) {
 
        try { 
            ClientGlobal.init(CONF_FILENAME);
 
            TrackerClient tracker = new TrackerClient();
            TrackerServer trackerServer = tracker.getTrackerServer();
            StorageServer storageServer = null;
            StorageClient storageClient = new StorageClient(trackerServer, storageServer);
            String fileIds[] = storageClient.upload_file(path, extName,null);
             
            System.out.println(fileIds.length); 
            System.out.println("组名:" + fileIds[0]); 
            System.out.println("路径: " + fileIds[1]);
            return  "/"+fileIds[0]+"/"+fileIds[1];
 
        } catch (Exception e) {
            e.printStackTrace();
            return  null;
        }
    }

    /**
     * 下载文件
     * @param groupName
     * @param fileName
     * @return
     */
    public static byte[] download(String groupName,String fileName) {
        try {
 
            ClientGlobal.init(CONF_FILENAME);
 
            TrackerClient tracker = new TrackerClient();
            TrackerServer trackerServer = tracker.getTrackerServer();
            StorageServer storageServer = null;
 
            StorageClient storageClient = new StorageClient(trackerServer, storageServer); 
            byte[] b = storageClient.download_file(groupName, fileName);
            return  b;
        } catch (Exception e) {
            e.printStackTrace();
            return  null;
        } 
    }
     
    /**
     * 删除文件
     * @param groupName
     * @param fileName
     */
    public static void delete(String groupName,String fileName){
        try { 
            ClientGlobal.init(CONF_FILENAME);
 
            TrackerClient tracker = new TrackerClient();
            TrackerServer trackerServer = tracker.getTrackerServer();
            StorageServer storageServer = null;
 
            StorageClient storageClient = new StorageClient(trackerServer, 
                    storageServer); 
            int i = storageClient.delete_file(groupName,fileName);
            System.out.println( i==0 ? "删除成功" : "删除失败:"+i);
        } catch (Exception e) {
            e.printStackTrace();
            throw  new RuntimeException("删除异常,"+e.getMessage());
        } 
    }
}

③:所需要的配置文件fdfs_client.conf 指向tracker-server服务端口

tracker_server=47.103.93.59:22122

④:点击提交按钮,将数据保存在数据库中

3. 创建页面静态化服务作为独立的微服务

①:集成eureka注册中心、config配置中心、集成feign

②:创建控制器,页面静态化的服务方法

@RestController
@RequestMapping("/static")
public class pageStaticAction {
    @Autowired
    private PagerServiceImpl pagerService;
    @PostMapping("/page")
    public AjaxResult pageStaticAction(@RequestParam("key")String key,@RequestParam("templateName")String templateName){
        pagerService.pageStaticAction(key,templateName);
        return AjaxResult.me();
    }
}

该方法接收两个参数:key表示页面静态化所需要的数据存储在redis中的key,templateName模板文件名字,对哪一个页面做页面静态化

③:集成feign,创建feign的接口以及托底类

@FeignClient(value = "static-page-server",fallbackFactory = StaticPageFignFallBack.class)
public interface StaticPageFignClient{
    @PostMapping("/static/page")
    AjaxResult pageStaticAction(@RequestParam("key")String key, @RequestParam("templateName")String templateName);

}
@Component
public class StaticPageFignFallBack implements FallbackFactory<StaticPageFignClient> {

    @Override
    public StaticPageFignClient create(Throwable throwable) {
        return new StaticPageFignClient() {
            @Override
            public AjaxResult pageStaticAction(String key, String name) {
                throwable.printStackTrace();
                return AjaxResult.me().setSuccess(false).setMessage("发生了一点小问题:["+throwable.getMessage()+"]");
            }
        };
    }
}

4. 以课程为例,在页面发生改变的时候通过feign调用页面静态化微服务

    java/**
     * 触发静态页面生成
     */
    public void pageStaticAction(){
        //1. 查询数据放到redis中
        List<CourseType> courseTypes = treeData();
        Map<String,Object> map = new HashMap<>();
        //模板文件中需要的属性名字
        map.put("courseTypes", courseTypes);
        AjaxResult result = redisFeignClient.set(RedisKeyConstants.COURSE_TYPE_PAGE_STATIC, JSON.toJSONString(map));
        if(!result.isSuccess()){
            throw new RuntimeException("数据存储失败!");
        }
        String templateName = "home";
        //2. 通过feign调用页面静态化微服务
        AjaxResult ajaxResult = staticPageFignClient.pageStaticAction(RedisKeyConstants.COURSE_TYPE_PAGE_STATIC,templateName);
        if(!ajaxResult.isSuccess()){
            throw new RuntimeException("页面静态化失败!");
        }
    }

5. 页面静态化中具体执行的逻辑

/**
     * 静态化页面生成
     * @param key redis存储的数据的key
     * @param templateName 模板文件名字
     */
    @Override
    public void pageStaticAction(String key, String templateName) {
        //2. 拿到模板
        Pager pager = baseMapper.getTemplateByName(templateName);
        if(pager==null && pager.getTemplateUrl().isEmpty()){
            throw new RuntimeException("所需模板获取失败!");
        }

        //3. 从fastdfs上下载模板
        byte[] templateFile = fastDFSFignClient.getTemplateFile(pager.getTemplateUrl());
        if(templateFile==null || templateFile.length==0){
            throw new RuntimeException("文件下载失败!");
        }

        //4. 获得windows文件临时存储路径 C:\Users\Han\AppData\Local\Temp\
        String systemTempPath =System.getProperty("java.io.tmpdir");
        //拼接zip文件存储路径:C:\Users\Han\AppData\Local\Temp\home.zip
        String zipPath = systemTempPath+templateName+".zip";
        try {
            FileCopyUtils.copy(templateFile, CreateFileUtils.createFile(zipPath));
        } catch (IOException e) {
            e.printStackTrace();
        }

        //5. 将下载的zip文件解压到当前目录 解压路径:C:\Users\Han\AppData\Local\Temp\templateFile\
        String unzipPath = systemTempPath+"templateFile/";
        try {
            ZipUtils.unZip(zipPath, unzipPath);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //6. 将下载解压后的模板与数据合并生成html文件
        //6.1 拿到数据
        AjaxResult ajaxResult = redisFeignClient.get(key);
        if(ajaxResult==null || !ajaxResult.isSuccess()){
            throw new RuntimeException("所需数据获取失败!");
        }

        String treeData = ajaxResult.getResultObj().toString();
        Map<String,Object> model = JSONObject.parseObject(treeData, Map.class);
        model.put("staticRoot", unzipPath);
        // model:数据对象 templateFilePathAndName 模板文件的物理路径 targetFilePathAndName 目标输出文件的物理路径
        String templateFilePathAndName = unzipPath+templateName+".vm";  //C:\Users\Han\AppData\Local\Temp\template\templateFile\home.vm
        String targetFilePathAndName = unzipPath+templateName+".html";  //C:\Users\Han\AppData\Local\Temp\template\templateFile\home.html
        VelocityUtils.staticByTemplate(model,templateFilePathAndName, targetFilePathAndName);

        //7. 将合并后的文件上传到fastdfs
        String htmlPathInFastdfs = null;
        try {
            byte[] bytes = FileCopyUtils.copyToByteArray(CreateFileUtils.createFile(targetFilePathAndName));
            if(bytes!=null && bytes.length>0){
                AjaxResult uploadResult = fastDFSFignClient.uploadByBytes(bytes,"html");
                if(!ajaxResult.isSuccess() || ajaxResult.getResultObj()==null){
                    throw new RuntimeException("html文件上传失败");
                }
                htmlPathInFastdfs = uploadResult.getResultObj().toString();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

fastdfs中上传及下载方法

@RestController
@RequestMapping("/fastdfs")
public class FastDFSController {

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public AjaxResult upload(MultipartFile file){

        String fileExtensionName = FilenameUtils.getExtension(file.getOriginalFilename());
        try {
            String filePath = FastDfsApiOpr.upload(file.getBytes(), fileExtensionName);
            return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
        } catch (IOException e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
        }
    }

    @PostMapping("/uploadByBytes")
    public AjaxResult uploadByBytes(@RequestBody byte[] fileBytes,@RequestParam("extName")String extName){
        try {
            String filePath = FastDfsApiOpr.upload(fileBytes, extName);
            return AjaxResult.me().setSuccess(true).setMessage("上传成功!").setResultObj(filePath);
        } catch (Exception e) {
            e.printStackTrace();
            return AjaxResult.me().setSuccess(false).setMessage("上传失败!");
        }
    }

    /**
     * 文件下载
     * @param fileUrl 文件路径
     * @return
     */
    @PostMapping("/download")
    public byte[] getTemplateFile(@RequestParam("fileUrl") String fileUrl){
        int index = fileUrl.indexOf("/");
        fileUrl = StringUtils.startsWith(fileUrl, "/") ?fileUrl.substring(index+1):fileUrl;

        String fileName = fileUrl.substring(fileUrl.indexOf("/")+1);
        String groupName =  fileUrl.substring(0,fileUrl.indexOf("/"));

        return FastDfsApiOpr.download(groupName, fileName);
    }
}

压缩文件解压工具类

            <dependency>
                <groupId>org.apache.ant</groupId>
                <artifactId>ant</artifactId>
                <version>1.7.1</version>
            </dependency>
package com.hanfengyi.hrm;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;

import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import org.apache.tools.zip.ZipOutputStream;

/**
 * <p>
 * ZIP工具包
 * </p>
 * <p>
 * 依赖:ant-1.7.1.jar
 * </p>
 * 
 * @author IceWee
 * @date 2012-5-26
 * @version 1.0
 */
public class ZipUtils {
    
    /**
     * 使用GBK编码可以避免压缩中文文件名乱码
     */
    private static final String CHINESE_CHARSET = "GBK";
    
    /**
     * 文件读取缓冲区大小
     */
    private static final int CACHE_SIZE = 1024;
    
    /**
     * <p>
     * 压缩文件
     * </p>
     * 
     * @param sourceFolder 压缩文件夹
     * @param zipFilePath 压缩文件输出路径
     * @throws Exception
     */
    public static void zip(String sourceFolder, String zipFilePath) throws Exception {
        OutputStream out = new FileOutputStream(zipFilePath);
        BufferedOutputStream bos = new BufferedOutputStream(out);
        ZipOutputStream zos = new ZipOutputStream(bos);
        // 解决中文文件名乱码
        zos.setEncoding(CHINESE_CHARSET);
        File file = new File(sourceFolder);
        String basePath = null;
        if (file.isDirectory()) {
            basePath = file.getPath();
        } else {
            basePath = file.getParent();
        }
        zipFile(file, basePath, zos);
        zos.closeEntry();
        zos.close();
        bos.close();
        out.close();
    }
    
    /**
     * <p>
     * 递归压缩文件
     * </p>
     * 
     * @param parentFile
     * @param basePath
     * @param zos
     * @throws Exception
     */
    private static void zipFile(File parentFile, String basePath, ZipOutputStream zos) throws Exception {
        File[] files = new File[0];
        if (parentFile.isDirectory()) {
            files = parentFile.listFiles();
        } else {
            files = new File[1];
            files[0] = parentFile;
        }
        String pathName;
        InputStream is;
        BufferedInputStream bis;
        byte[] cache = new byte[CACHE_SIZE];
        for (File file : files) {
            if (file.isDirectory()) {
                zipFile(file, basePath, zos);
            } else {
                pathName = file.getPath().substring(basePath.length() + 1);
                is = new FileInputStream(file);
                bis = new BufferedInputStream(is);
                zos.putNextEntry(new ZipEntry(pathName));
                int nRead = 0;
                while ((nRead = bis.read(cache, 0, CACHE_SIZE)) != -1) {
                    zos.write(cache, 0, nRead);
                }
                bis.close();
                is.close();
            }
        }
    }
    
    /**
     * <p>
     * 解压压缩包
     * </p>
     * 
     * @param zipFilePath 压缩文件路径
     * @param destDir 压缩包释放目录
     * @throws Exception
     */
    public static void unZip(String zipFilePath, String destDir) throws Exception {
        ZipFile zipFile = new ZipFile(zipFilePath, CHINESE_CHARSET);
        Enumeration<?> emu = zipFile.getEntries();
        BufferedInputStream bis;
        FileOutputStream fos;
        BufferedOutputStream bos;
        File file, parentFile;
        ZipEntry entry;
        byte[] cache = new byte[CACHE_SIZE];
        while (emu.hasMoreElements()) {
            entry = (ZipEntry) emu.nextElement();
            if (entry.isDirectory()) {
                new File(destDir + entry.getName()).mkdirs();
                continue;
            }
            bis = new BufferedInputStream(zipFile.getInputStream(entry));
            file = new File(destDir + entry.getName());
            parentFile = file.getParentFile();
            if (parentFile != null && (!parentFile.exists())) {
                parentFile.mkdirs();
            }
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos, CACHE_SIZE);
            int nRead = 0;
            while ((nRead = bis.read(cache, 0, CACHE_SIZE)) != -1) {
                fos.write(cache, 0, nRead);
            }
            bos.flush();
            bos.close();
            fos.close();
            bis.close();
        }
        zipFile.close();
    }
    
}

创建文件工具类

public class CreateFileUtils {
    /**
     * 创建文件
     * @param filePath
     * @return
     */
    public static File createFile(String filePath){
        File file = new File(filePath);
        if(!file.exists()){
            //创建父路径
            if(!file.getParentFile().exists()){
                file.getParentFile().mkdirs();
            }
            try {
                file.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return file;
    }
}

需要用到的模板引擎Velocity

            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>2.0</version>
            </dependency>

需要用到的模板引擎Velocity工具类

package com.hanfengyi.hrm;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.Properties;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;

public class VelocityUtils {
    private static Properties p = new Properties();
    static {
        p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
        p.setProperty(Velocity.ENCODING_DEFAULT, "UTF-8");
        p.setProperty(Velocity.INPUT_ENCODING, "UTF-8");
        p.setProperty(Velocity.OUTPUT_ENCODING, "UTF-8");
    }
    
    /**
     * 返回通过模板,将model中的数据替换后的内容
     * @param model
     * @param templateFilePathAndName
     * @return
     */
    public static String getContentByTemplate(Object model, String templateFilePathAndName){
        try {
            Velocity.init(p);
            Template template = Velocity.getTemplate(templateFilePathAndName);
            VelocityContext context = new VelocityContext();
            context.put("model", model);
            StringWriter writer = new StringWriter();
            template.merge(context, writer);
            String retContent = writer.toString();
            writer.close();
            return retContent;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 根据模板,静态化model到指定的文件 模板文件中通过访问model来访问设置的内容
     * 
     * @param model
     *            数据对象
     * @param templateFilePathAndName
     *            模板文件的物理路径
     * @param targetFilePathAndName
     *            目标输出文件的物理路径
     */
    public static void staticByTemplate(Object model, String templateFilePathAndName, String targetFilePathAndName) {
        try {
            Velocity.init(p);
            Template template = Velocity.getTemplate(templateFilePathAndName);
            
            VelocityContext context = new VelocityContext();
            context.put("model", model);
            FileOutputStream fos = new FileOutputStream(targetFilePathAndName);
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));// 设置写入的文件编码,解决中文问题
            template.merge(context, writer);
            writer.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 静态化内容content到指定的文件
     * 
     * @param content
     * @param targetFilePathAndName
     */
    public static void staticBySimple(Object content, String targetFilePathAndName) {
        VelocityEngine ve = new VelocityEngine();
        ve.init(p);
        String template = "${content}";
        VelocityContext context = new VelocityContext();
        context.put("content", content);
        StringWriter writer = new StringWriter();
        ve.evaluate(context, writer, "", template);
        try {
            FileWriter fileWriter = new FileWriter(new File(targetFilePathAndName));
            fileWriter.write(writer.toString());
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

6. 集成MQ,使用定向的方式发布消息,不声明交换机和队列,在ngnix集成的微服务中声明

        //8. 集成RabbitMQ,发布消息:文件在fastdfs上的路径,以及站点路径
        Site site = siteMapper.selectById(pager.getSiteId());

        Map<String,String> map = new HashMap<>();
        map.put("htmlPath", htmlPathInFastdfs);
        map.put("sitePath", pager.getPhysicalPath());
       rabbitTemplate.convertAndSend(RabbitMQConstants.EXCHANGE_NAME_DIRECT, site.getSn(), JSON.toJSONString(map));
    }

7.ngnix站点(消费者)集成MQ,声明交换机和队列,侦听队列消息,拿到fastdfs上的路径下载输出到站点路径

①:导入MQ的包

<!--spirngboot集成rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

②:创建配置类

@Configuration
public class RabbitMQConfig {

    @Value("${pageStatic.routingKey}")
    String routingKey;
    //交换机
    @Bean
    public Exchange directExchange(){
        return ExchangeBuilder.directExchange(RabbitMQConstants.EXCHANGE_NAME_DIRECT).build();
    }

    //队列
    @Bean
    public Queue pageStaticQueue(){
        return new Queue(RabbitMQConstants.QUEUE_NAME_PAGE_STATIC);
    }
    //绑定队列到交换机
    @Bean
    public Binding smsBinding(){
        return BindingBuilder.bind(pageStaticQueue()).to(directExchange()).with(routingKey).noargs();
    }

}

③:创建配置文件

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1010/eureka/ #注册中心服务端的注册地址
  instance:
    prefer-ip-address: true #使用ip进行注册
    instance-id: proxy-server:2070  #服务注册到注册中心的id

server:
  port: 2070
spring:
  application:
    name: proxy-server
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
    listener:
      simple:
        acknowledge-mode: manual #手动签收

④:消费方法

@Component
public class Consumer {
    @Autowired
    private FastDFSFignClient fastDFSFignClient;
    @RabbitListener(queues = {RabbitMQConstants.QUEUE_NAME_PAGE_STATIC})
    public void MQListener(String msg, Message message, Channel channel){
        //1. 从map中取出文件在fastdfs上的路径,以及站点路径
        System.out.println(msg);
        Map<String,String> map = JSONObject.parseObject(msg, Map.class);
        String htmlPath = map.get("htmlPath");
        String sitePath = map.get("sitePath");
    
        //2. 通过feign调用fastdfs,把html文件下载下来下载到对应站点路径
        byte[] templateFile = fastDFSFignClient.getTemplateFile(htmlPath);
        if(templateFile!=null || templateFile.length>0){
            try {
                FileCopyUtils.copy(templateFile, CreateFileUtils.createFile(sitePath));
                //3. 触发手动签收
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容