相关文章推荐
怕老婆的回锅肉  ·  Android 27+ ...·  2 年前    · 
yiqiwuliao.com

为用户提供将连载(网页)导出为 PDF 功能,以便于用户更放心地记录,不用担心数据丢失。并且为 APP 端提供接口,当用户点击导出按钮时,提交导出任务到后台,成功导出后,通过私信告知用户。

因为 Java 在转换 PDF 方面效果不够好,加上之前了解了下网页转换为 PDF,发现 phantomjs 是非常棒的一个工具。于是这次就直接选了该 js 进行转换。

主要的思路是这样的:

  • 安装 phantomjs
  • APP 端传入需要导出的 URL,请求导出接口
  • 服务器端接收到导出请求,根据 URL 的参数来判断导出的任务个数(因为有些连载会有几百个阶段,如果一次导出的话,会非常慢且生成的文件巨大),并且这是一个耗时操作,所以异步将任务添加到分布式队列中,同时返回成功状态码给前端,以免前端等待。
  • 消息队列的消费者收到任务,开始调用 shell 命令,执行 phantomjs
  • phantomjs 转换完毕后,将转成成的 pdf 上传到七牛(我们提供给用户的是一个地址,这样用户下载时就不消耗自己服务器带宽了)
  • 上传七牛成功后,消息队列手动 ACK,并将导出的结果存入数据库,同时发送私信告知用户,已经成功导出。
  • 这就是主要的实现思路。

    安装 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,看看效果如何。

    toPdf.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 的效果如下:

    非客观的我.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 返回码以及权限不足问题。

    以下资料可作为参考

    255错误码

    127错误码

    【权限不足】在 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。有兴趣的可以自己去了解下。