Java整合Thymeleaf和wkhtmltopdf实现HTML导出PDF

前端实现导出 PDF 产品报告,存在几个问题:

  1. 是图片版的 PDF;

  2. PDF 太大,会卡;

  3. 可能会把文字裁剪分页;

  4. 无法满足平台提供 Api 接口服务。

核心就是问题3和问题4,于是,考虑后端服务实现导出 PDF 产品报告的方案。

Java 实现 HTML 转 PDF 技术选型


推荐使用 wkhtmltopdf, Itext,但 wkhtmltopdf 开源免费,Itext 需要考虑版权

参考:https://blog.csdn.net/weixin_43981813/article/details/128135730

参考:https://www.cnblogs.com/IT-study/p/13706690.html

技术实现方案

技术采用模板引擎 + PDF 插件实现。开发好页面模板,Thymeleaf 模板引擎渲染静态 HTML 文件,wkhtmltopdf 将静态的 HTML 生成 PDF 文件。整体方案流程如下:


后台使用 Thymeleaf 模板生成 Html 报告页面

PDF 接口根据 ID 查询报告数据

调用 wkhtmltopdf 工具 将 Html 转为 PDF 文件

关于 wkhtmltopdf

参数文档: https://wkhtmltopdf.org/usage/wkhtmltopdf.txt

wkhtmltopdf 安装

Yum 安装(可能是老版本存在bug,不推荐)

yum -y install wkhtmltopdf

rpm 包安装

下载最新按照包,如

wget https://objects.githubusercontent.com/github-production-release-asset-2e65be/131323182/4c2dd800-ab8e-11ea-95aa-09875726406d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20230904%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20230904T082059Z&X-Amz-Expires=300&X-Amz-Signature=8eb5914f5551c3f9a454537895ab41c3884fcb447ca4babf6c57e27fefb46b41&X-Amz-SignedHeaders=host&actor_id=39854904&key_id=0&repo_id=131323182&response-content-disposition=attachment%3B%20filename%3Dwkhtmltox-0.12.6-1.centos8.x86_64.rpm&response-content-type=application%2Foctet-stream

先安装依赖包

yum install -y fontconfig libX11 libXext libXrender libjpeg libpng xorg-x11-fonts-75dpi xorg-x11-fonts-Type1

wkhtmltox 安装

rpm -ivh wkhtmltox-0.12.6-1.centos8.x86_64.rpm

若需要路径执行,可配置

cp /usr/local/bin/wkhtmltopdf /usr/bin/wkhtmltopdf

内网安装

先下载依赖包到指定目录,例如下载 openssl 依赖包到指定目录

yum install --downloadonly --downloaddir=/usr/soft/wktooltopdf/ openssl

之后,拷贝依赖包到内网环境,执行命令

rpm -ivh --force --nodeps *.rpm

rpm -ivh --force --nodeps *.rpm

常见问题

缺少依赖包

手动安装依赖包

FAQ-linux 安装 wkhtmltopdf 中文乱码或者空白解决方法

参考:https://www.cnblogs.com/jluo/p/17403785.html

安装中文字体,或复制已有字体

打开windows c:\Windows\fonts\simsun.ttc

拷贝到linux服务器/usr/share/fonts/目录下,再次生成pdf中文显示正常

出现错误: wkhtmltopdf:cannot connect to X server

参考:https://www.jianshu.com/p/2cfc02961528

需再安装xvfb

yum install xorg-x11-server-Xvfb

在 /usr/bin/ 目录下生成脚本 wkhtmltopdf.sh 并写入命令

sudo vim /usr/bin/wkhtmltopdf.sh 

命令:

xvfb-run -a --server-args="-screen 0, 1024x768x24" /usr/bin/wkhtmltopdf -q $*

更改文件权限并建立连接

chmod a+x /usr/bin/wkhtmltopdf.sh

ln -s /usr/bin/wkhtmltopdf.sh /usr/local/bin/wkhtmltopdf

中文字体安装

若出现中文乱码,则可能是缺少字体

阿里巴巴普惠体2.0,免费无版权,好用

下载地址: https://done.alibabadesign.com/puhuiti2.0

介绍说明: https://fonts.adobe.com/fonts/alibaba-puhuiti

设置字体集

font-family: alibaba-puhuiti, sans-serif;

font-style: normal;

font-weight: 300;

开发代码及配置

静态资源目录位于 *-model 工程下的资源文件,包括以下目录

templates/ - 模板文件

static/ - 静态资源文件

若前端有修改调整,需将更新的文件复制到 *-model 工程下对应目录,

静态资源复制方案1:maven 插件配置, 用于复制公共的资源

pom.xml 增加插件配置

<plugin>  <!-- 该插件的作用是用于复制 PDF 模板资源文件 -->

    <artifactId>maven-resources-plugin</artifactId>

    <executions>

        <execution>

            <id>copy-resources</id>

            <phase>package</phase>

            <goals>

                <goal>copy-resources</goal>

            </goals>

            <configuration>

                <resources>

                    <resource>

                        <directory>../../*-model/src/main/resources</directory>  <!-- 指定相对路径,复制 *-model 下的模板静态资源 -->

                        <includes>  <!-- 复制模板文件和静态资源文件-->

                            <include>templates/**</include>

                            <include>static/**</include>

                        </includes>

                    </resource>

                </resources>

                <outputDirectory>src/main/resources</outputDirectory>  <!-- 指定输出目录,复制到当前工程资源模型下,用于下一步打包 -->

                <skip>true</skip>  <!-- 跳过执行,已配置了 package.xml,直接复制到打包文件 -->

            </configuration>

        </execution>

    </executions>

</plugin>

静态资源复制方案2:自定义的打包配置,增加资源复制

推荐使用此方法,直接复制资源并打包到目标 zip 包

路径:/src/main/assemble/package.xml,增加配置

<fileSet>  <!-- 该插件的作用是用于复制 PDF 模板资源文件 -->

      <directory>../../*-model/src/main/resources</directory>

      <outputDirectory>\</outputDirectory>

      <includes>  <!-- 复制模板文件和静态资源文件-->

          <include>templates/**</include>

          <include>static/**</include>

      </includes>

  </fileSet>

Java 工具类

由于模板引擎对 JS 的支持有限,固增加 Java 工具类,用于模板中处理数据(模板引擎是在服务端执行,可执行 Java 代码)

参考 HtmlThymeleafHelper 配置, 注意 ModelAndView 中返回工具列

后端开发 pom.xml 依赖

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-thymeleaf</artifactId>

</dependency>

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-web</artifactId>

</dependency>

开发模板

<!DOCTYPE html>

<html  xmlns:th="http://www.thymeleaf.org">

<head>

    <meta charset="UTF-8">

    <title>title</title>

</head>

<body>

Hello Thymeleaf

<div th:text="${name}">张三(离线数据)</div>

</body>

</html>

后端接口处理 ModelAndView

// 接口返回 ModelAndView, 指定模板, 设置数据

@GetMapping("/template/render")

public ModelAndView templateReportDetail(HttpServletRequest request, @RequestParam String reportId) {

    this.initJavaEnv(request);

    return this.renderModelAndView("TemplateReportDetail", reportId);

}

// 通过 Session 设置 Java 对象,用于模板中执行 Java 方法

private void initJavaEnv(HttpServletRequest request) {

    HtmlThymeleafHelper helper = Singleton.get(HtmlThymeleafHelper.class);

    request.getSession().setAttribute("helper", helper);

}

// 创建一个模型视图对象

ModelAndView mav = new ModelAndView();

// 获取到查询的数据

Object data = ret.getRetObject();

// 将数据放置到ModelAndView对象view中,第二个参数可以是任何java类型

mav.addObject("sourceData", data);

// 放入模板路径

mav.setViewName("template");

// 返回ModelAndView对象mav

return mav;

SpringBoot yml 配置

spring:

  mvc:

    # 添加static文件夹下其他文件夹可访问

    static-path-pattern: /project/static/**

    # 自定义配置项,指定模板路径

    base-template-path: /project/template

  thymeleaf:

    cache: true

    mode: HTML5

    suffix: .html

    prefix: classpath:/templates/

    encoding: UTF-8

    servlet:

      content-type: text/html

关于模板引擎 Thymeleaf

什么是Thymeleaf?

Thymeleaf 官网是这么解释的:Thymeleaf is a modern server-side Java template engine for both web and standalone environments.

译过来就是:Thymeleaf是适用于Web和独立环境的现代服务器端Java模板引擎

什么是模板引擎?

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的html文档。从字面上理解模板引擎,最重要的就是模板二字,这个意思就是做好一个模板后套入对应位置的数据,最终以html的格式展示出来,这就是模板引擎的作用。

不仅如此,在Java中模板引擎还有很多,模板引擎是动态网页发展进步的产物,在最初并且流传度最广的jsp它就是一个模板引擎。jsp是早期官方标准的模板,但是由于jsp的缺点比较多也挺严重的,所以很多人弃用jsp选用第三方的模板引擎,市面上开源的第三方的模板引擎也比较多,有Thymeleaf、FreeMaker、Velocity等模板引擎受众较广。

听完了模板引擎的介绍,相信你也很容易明白了模板引擎在web领域的主要作用:让网站实现界面和数据分离,这样大大提高了开发效率,让代码重用更加容易。


Model、ModelMap、ModelAndView

Model

一般来说,可以用Model来接收各种类型的数据,如果使用来接收一组数据List,那么这个时候的Model实际上是ModelMap

ModelMap

主要用于传递控制方法处理数据到结果页面,也就是说我们把结果页面上需要的数据放到ModelMap对象中即可,

他的作用类似于request对象的setAttribute方法的作用:用来在一个请求过程中传递处理的数据

ModelMap或者Model通过addAttribute方法向页面传递参数

ModelAndView

指模型和视图的集合,既包含 模型 又包含 视图

Model和 ModelMap 无需用户自己创建,而且需要return 返回指定的页面路径

Model和 ModelMap 无需用户自己创建,而且需要return 返回指定的页面路径

public String listCategory2(Model model) {

    // 接收查询的信息

    List<Category> cs2= categoryService.list();

    // 封装了查询的数据

    model.addAttribute("test", cs2);

    //重要!!需要给出返回model跳转的路径

    return "listCategory2";

}

ModelAndView的实例是需要我们手动new的,这也是和ModelMap的一个区别。

而且,ModelAndView 可以自己寻址,只需要return 返回其对象即可。

public ModelAndView listCategory(){

  //创建一个模型视图对象

    ModelAndView mav = new ModelAndView();

    //获取到查询的数据

    List<Category> cs= categoryService.list();

    // //将数据放置到ModelAndView对象view中,第二个参数可以是任何java类型

    mav.addObject("cs", cs);

    // 放入jsp路径

    mav.setViewName("listCategory");

    //返回ModelAndView对象mav

    return mav;

}

参考:https://cloud.tencent.com/developer/article/1698750

Thymeleaf 常用标签

标签作用示例

th:id替换id<input th:id="${user.id}"/>

th:text文本替换<p text:="${user.name}">bigsai</p>

th:utext支持html的文本替换<p utext:="${htmlcontent}">content</p>

th:object替换对象<div th:object="${user}"></div>

th:value替换值<input th:value="${user.name}" >

th:each迭代<tr th:each="student:${user}" >

th:href替换超链接<a th:href="@{index.html}">超链接</a>

th:src替换资源<script type="text/javascript" th:src="@{index.js}"></script>

七大基础对象:

${#ctx} 上下文对象,可用于获取其它内置对象。

${#vars}: 上下文变量。

${#locale}:上下文区域设置。

${#request}: HttpServletRequest对象。

${#response}: HttpServletResponse对象。

${#session}: HttpSession对象。

${#servletContext}: ServletContext对象。

常用的工具类:

#strings:字符串工具类

#lists:List 工具类

#arrays:数组工具类

#sets:Set 工具类

#maps:常用Map方法。

#objects:一般对象类,通常用来判断非空

#bools:常用的布尔方法。

#execInfo:获取页面模板的处理信息。

#messages:在变量表达式中获取外部消息的方法,与使用#{...}语法获取的方法相同。

#uris:转义部分URL / URI的方法。

#conversions:用于执行已配置的转换服务的方法。

#dates:时间操作和时间格式化等。

#calendars:用于更复杂时间的格式化。

#numbers:格式化数字对象的方法。

#aggregates:在数组或集合上创建聚合的方法。

#ids:处理可能重复的id属性的方法。

引入css(必须要在标签中加上rel属性)

<link rel="stylesheet" th:href="@{index.css}">

<link th:href="@{/static/css/index.css}" type="text/css" rel="stylesheet">

引入JavaScript:

<script type="text/javascript" th:src="@{index.js}"></script>

<script type="text/javascript" th:src="@{/js/jquery.js}"></script>

超链接:

<a th:href="@{index.html}">超链接</a>

变量表达式: ${...}

在Thymeleaf中可以通过${…}进行取值,这点和ONGL表达式语法一致

取JavaBean对象:

使用${对象名.对象属性}或者${对象名['对象属性']}来取值

如果该JavaBean如果写了get方法,也可以通过get方法取值例如${对象.get方法名}

<td th:text="${user.name}"></td>

<td th:text="${user['age']}"></td>

<td th:text="${user.getDetail()}"></td>

取List集合(each):

因为List集合是个有序列表,要遍历List对其中对象取值,而遍历需要用到标签:th:each,

具体使用为 <tr th:each="item:${userlist}">,其中item就相当于遍历每一次的对象名

<table bgcolor="#ffe4c4" border="1">

    <tr th:each="item:${userlist}">

        <td th:text="${item}"></td>

    </tr>

</table>

直接取Map:

很多时候我们不存JavaBean而是将一些值放入Map中,再将Map存在Model中,我们就需要对Map取值,

可以 ${Map名['key']} 取值。也可以 ${Map名.key} 取值,当然也可以 ${map.get('key')}(java语法)取值

<table bgcolor="#8fbc8f" border="1">

    <tr>

        <td>place:</td>

        <td th:text="${map.get('place')}"></td>

    </tr>

    <tr>

        <td>feeling:</td>

        <td th:text="${map['feeling']}"></td>

    </tr>

</table>

参考:https://developer.aliyun.com/article/769977

Thymeleaf 控制处理

<input type="text" name="menuName" disabled th:value="${result?.data?.menuName}" class="layui-input">

? 会判断对象是否为空,如果为空就不会继续取值

SPEL处理 null 值

变量为 null 时,显示默认值

name?:'Unknown'

当 name 变量为 null 时,显示值 Unknown。等价于 name?name:'Unknown'。

对象为 null 时,避免调用方法或属性出错

placeOfBirth?.city

当 placeOfBirth 为 null 时,不再继续调用属性 city。

code?.toUpperCase()

当 code 为 null 时,不再继续调用方法 toUpperCase。

Map 获取的元素为 null

当 map 中没有名为 name 的元素时,这样写会报错 map.name。

安全的写法是这样:map['name']。

如果 map 中的元素为对象时,可以这样写:map['user']?.name。

List 类型数组越界

数组越界时,错误是这样的:

Caused by: org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "slist[2].score" (template: "exam/papers/edit" - line 117, col 92)

Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1025E: The collection has '1' elements, index '2' is invalid

SPEL 是这样的 slist[2].score,但 slist 不够3个元素(EL1025E: The collection has '1' elements, index '2' is invalid),因此数组越界了。

解决办法:添加数组大小的判断。上面的情况下,用 #lists.size(slist)>=3?slist[2]?.score:0 替换 slist[2].score

参考:https://blog.csdn.net/sayyy/article/details/109385456

参考:https://zhuanlan.zhihu.com/p/90642654

Thymeleaf 调用 Java 方法

1. Java 对象示例存入 Thymeleaf Context 域中,代码层面即为:将实例对象存入Request对象中

MethodService md = new MethodService();

mmap.put("methodService",md);

mmap.put("proofsList",proofsList);

<label class="checkbox-inline i-checks"  th:each="data : ${list}"> 

    <input th:attr="checked=${methodService.contains(data.id,proofsList)?true:false}" type="checkbox" name="proofs[]"  th:value="${data.id}" id="inlineCheckbox1" />

</lable>

Thymeleaf 动态添加样式

<li class="treeview" th:classappend="${tree == 'index'}?'active'"></li>

或者

<li class="treeview" th:classappend="${tree == 'index'?'active':''}"></li>

动态绑定样式

<li th:class="${tree == 'index'?'active':''}"></li>

Thymeleaf 数组处理

// 数组长度

<p>The greatest <span th:text="${#arrays.length(continents)}"></span> continents.</p>

// 数组包含

<p>Europe is a continent: <span th:text="${#arrays.contains(continents, 'Europe')}"></span>.</p>

// 数组判空

<p>Array of continents is empty <span th:text="${#arrays.isEmpty(continents)}"></span>.</p>

Thymeleaf 遍历生成复杂的表格

<table class="layui-table" id="tabRank">

    <tr>

        <th colspan="2">机构</th>

        <th colspan="2">年份</th>

        <th colspan="2">得分</th>

        <th colspan="2">全球排名</th>

    </tr>

    <div th:remove="tag" th:if="*{#lists.isEmpty(institution)}">

        <tr>

            <td colspan="8" style="text-align: center">无排名信息</td>

        </tr>

    </div>

    <div th:remove="tag" th:if="*{not #lists.isEmpty(institution)}" th:each="institution:${institution}">

        <tr>

            <td colspan="2" rowspan="4" th:text="${institution.institution}"></td>

            <tr th:each="rank:${schoolRank}" th:if="${rank.schoolRankInstitution}==${institution.institution}">

                <td colspan="2" th:text="${rank.schoolRankYears}"></td>

                <td colspan="2" th:text="${rank.schoolRankScore}"></td>

                <td colspan="2" th:text="${rank.schoolRankGlobal}"></td>

            </tr>

        </tr>

    </div>

</table>

th:remove="tag"

它在这的作用是生成表格后把div删除,但不删除子元素

th:if="*{#lists.isEmpty(institution)}"

判断从后台获取的数据为空,空则不渲染 tr 标签

th:if="*{not #lists.isEmpty(institution)}"

判断从后台获取的数据不为空,不为空则渲染 tr 标签

<div th:remove="tag" th:each="downPriceEntry,stats:${appPriceInfoVO.downPriceMap}"

    th:with="appName = ${downPriceEntry.key}, appChangeNum = ${downPriceEntry.value.size()},

          appInfo0 = ${downPriceEntry.value.get(0)}, downPriceList = ${downPriceEntry.value}">

    <tr th:if="${appPriceInfoVO.downNum}>0">

        <td class="btbg1" th:text="价格下降" th:rowspan="${appPriceInfoVO.downNum}" th:if="${stats.first}"></td>

        <td th:text="${appName}" th:rowspan="${appChangeNum}"

            th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${appInfo0.price}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${appInfo0.version}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${appInfo0.createTime}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${appInfo0.language}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'">

            <a th:href="${appInfo0.url}" target="_blank" th:text="${appInfo0.name}"></a>

        </td>

    </tr>

    <tr th:each="downPriceAppInfo,stat : ${downPriceList}" th:if="${!stat.first}">

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${downPriceAppInfo.price}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${downPriceAppInfo.version}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${downPriceAppInfo.createTime}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'" th:text="${downPriceAppInfo.language}"></td>

        <td th:class="${stats.index % 2 == 0} ? 'btbg4':'btbg3'">

            <a th:href="${downPriceAppInfo.url}" target="_blank" th:text="${downPriceAppInfo.name}"></a>

        </td>

    </tr>

</div>

th:remove:会移除该标签行,不会移除其子标签

th:each:迭代集合或者数组

th:with:临时变量的声明

colspan 合并单元格 列

rowspan 合并单元格 行

常见问题

wkhtmltopdf 生成 PDF 的表格行内出现分页符、表头重复、截断等

增加表格样式

thead {

    display: table-row-group;

}

tr {

    page-break-before: always;

    page-break-after: always;

    page-break-inside: avoid;

}

table {

    word-wrap: break-word;

}

table td {

    word-break: break-all;

}

说明:wkhtmltopdf 对表格的支持很差,会导致文件很大,长表格兼容性等问题

参考:https://blog.csdn.net/yellowatumn/article/details/87864601

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

推荐阅读更多精彩内容