为用户提供将连载(网页)导出为 PDF 功能,以便于用户更放心地记录,不用担心数据丢失。并且为 APP 端提供接口,当用户点击导出按钮时,提交导出任务到后台,成功导出后,通过私信告知用户。
因为 Java 在转换 PDF 方面效果不够好,加上之前了解了下网页转换为 PDF,发现 phantomjs 是非常棒的一个工具。于是这次就直接选了该 js 进行转换。
主要的思路是这样的:
phantomjs
shell
命令,执行
phantomjs
phantomjs
转换完毕后,将转成成的 pdf 上传到七牛(我们提供给用户的是一个地址,这样用户下载时就不消耗自己服务器带宽了)
这就是主要的实现思路。
安装 phantomjs
wget wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
我们先来简单测试下该 js,看看效果如何。
具体代码为:
'use strict';
var page = require('webpage').create()
, system = require('system')
, args = system.args
, url = args.length > 1 ? args[1] : 'http://www.lianzai.me/toPdf/666666/10192/1.html'
, filename = args.length > 2 ? args[2] : 'tmp';
page.paperSize = {
format: 'A4',
header: {
height: "2cm",
contents: phantom.callback(function(pageNum, numPages) {
return "";
footer: {
height: "2.5cm",
contents: phantom.callback(function(pageNum, numPages) {
return "<h1 style='position: relative;top:35px;font-size:13px;color:#999;text-align:center;font-weight:400'><span>" + pageNum + " / " + numPages + "</span></h1>";
url = decodeURIComponent(url);
page.open(url, function (status) {
console.log(status);
if (status === 'success') {
page.render('/testpdf/' + filename + '.pdf');
phantom.exit();
参数中如果没有传递 url 和 fileName,会有默认值。
我们把 phanmotjs
加入到 path
中,以便可以在终端直接使用 phantomjs
。
vim /etc/profile
export PATH=/usr/software/phantomjs-2.1.1-linux-x86_64/bin:$PATH
source /etc/profile
phantomjs toPdf.js http://www.lianzai.me/toPdf/123456/14456/1.html 非客观的我
转换成 PDF 的效果如下:
部分截图:
可以看到效果是非常不错的。
定义 API
public static void export(Long uid, @Required String url, String uidSid) {
paramError();
// 判断是否为 vip
Integer vipStatus = userInfoLogic.getVipStatus(uid);
if (!UserInfo.VipStatus.VIP.getIndex().equals(vipStatus)) {
renderJsonFail(ReturnCode.ONLY_VIP_ACCESS.getCode(), ReturnCode.ONLY_VIP_ACCESS.getMsg());
Long planId = extractPlanId(url);
long stageCount = planStageLogic.countByUidAndPlanId(uid, planId, 0);
if (stageCount > 0) {
Date now = new Date();
String taskId = Codec.UUID().split("-")[0];
int pageCount = calculatePageCount(stageCount);
Cache.set(taskId, pageCount, "10h");
for (int i = 1; i <= pageCount; i++) {
String url1 = url + "/" + i + ".html";
AsyncOperation.createExportPdfMsg(taskId, uid, planId, url1, now, QueueTypeEnum.EXPORT_PDF);
renderJsonSuccess();
这主要是自己的业务逻辑,主要看
AsyncOperation.createExportPdfMsg(taskId, uid, planId, url1, now, QueueTypeEnum.EXPORT_PDF)
,这里将任务细分为几个任务,然后添加到分布式消息队列中。详细代码就不贴了,都是一些基础的类。
Java 调用 Shell
在上一个步骤中,我们将任务添加到分布式消息队列中,然后现在需要进行消费。这个过程,主要是利用 Java 调用 Shell 命令,让 Java 执行我们的导出脚本。
Java 调用 Shell,关键代码就是
Process process = Runtime.getRuntime().exec(command);
process.waitFor();
该方法会返回一个 int 值,当返回结果为 0 的时候,则表示成功。
在这个问题上,我遇到过 127,255 返回码以及权限不足问题。
以下资料可作为参考
【权限不足】在 command 前面加上 chmod 777
即可解决。
上传到七牛
这个调用七牛 API 即可,上传完毕后,将七牛返回的地址存入数据库,同时向用户发送完成的私信通知。
然后再告诉消息队列,说该条消息已经被成功处理了,可以废弃掉了。
千万要注意执行的脚本路径,不然会出各种意外的问题,例如权限不足,或者没有报任何错误,但是就是没有效果。
中文乱码问题
linux 上可能没有安装可用的中文字体,导出来的pdf中文就为空白了。下载一个字体,进行解压,然后建立软链接
wget http://dlc2.pconline.com.cn/filedown_367689_7048847/f9qOLERr/simsun.zip
unzip simsun.zip
ln -s /usr/share/fonts/truetype/simsun.ttf
以上大体上就完成了我们的需求,还有很多业务逻辑需要自己去考虑,关于这里使用的分布式消息队列,是 RabbitMQ
。有兴趣的可以自己去了解下。