前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >itext实现pdf自动定位合同签订

itext实现pdf自动定位合同签订

作者头像
老梁
发布于 2019-09-10 09:33:46
发布于 2019-09-10 09:33:46
2.4K00
代码可运行
举报
运行总次数:0
代码可运行

需求

  1. 需要实现如下效果(最终效果)

思考

  1. 需求方的要求就是实现签订合同,实现方法不限,但过程中又提出需要在签章的过程中把签订日期的文字也打上去,这就有点坑了~
  2. 一开始的想法是想办法定位需要签名的位置,事实上同类app实现方式就是这样,在前端实现签名位置定位,把位置信息发给后端,后端就可以很方便把印章放上去。
  3. 但现实是现在前端不靠谱,暂时不能提供这样的功能;而且日期信息的填写也需要定位,这怎么办?用户不会手动去定位日期的位置,最多会调整下签名的位置才合理
  4. 然后我研究了下itext的api,并讨论决定尾部签名部分我们自己做。也就是上图中的下半部分的所有内容,包括甲方乙方,日期,签章等都通过程序自动定位上去
  5. 这样的想法遇到的难点,首先是y轴的定位问题。首先要找到文档的尾行在哪,在适当的距离进行文字的填写。我没有找到可以直接在文档末尾添加文字的api,如果各位知道麻烦指教一下

步骤

  1. 因为有上述的问题,我首先考虑要找到尾行的文字才会考虑写代码。通过api研究,可以通过itext的监听器遍历文本拿到尾行文字等信息
  2. x周位置根据页面宽度调整
  3. 文字大小和字体类型问题。字体类型是我现在也没解决的,我没找到获取pdf文档字体类型和大小的api,请指教
  4. 因为没找到api所以我用的最笨的方法,通过获取字体的高度来确定字体大小,这样的文字写出来差别不会太大。至于字体,只能认为规定,合同字体统一宋体。
  5. 过程中还遇到的问题就是字体左边距对齐问题,很明显甲乙方在一行上,中间用空格来分割的话会很不标准。所以我最终决定用table,且左右边签名和文字分开进行写入。也就是甲签的时候写左半部分,乙签的时候写右半部分。当签完后就是上图的效果
  6. 说了这么多接下来直接上工具代码吧,如果要使用,直接把几个类代码复制过去,把字体路径换成自己的,文件路径改下就可以在main方法运行测试了

上代码

  1. PdfParser类,主要实现类,包含了main方法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zhiyis.framework.util.itext;

import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.kernel.font.PdfFont;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.geom.Vector;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.parser.EventType;
import com.itextpdf.kernel.pdf.canvas.parser.PdfDocumentContentParser;
import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData;
import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo;
import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.element.Cell;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.element.Table;
import com.zhiyis.common.utils.DateUtil;
import com.zhiyis.common.utils.Sysconfig;
import com.zhiyis.framework.util.FileUtil;
import com.zhiyis.framework.util.SignPdf;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;


/**
 * @author laoliangliang
 * @date 2018/11/23 15:03
 */
@Slf4j
public class PdfParser {

    private Sysconfig sysconfig;

    public PdfParser() {
    }

    public PdfParser(Sysconfig sysconfig) {
        this.sysconfig = sysconfig;
    }

    public enum SignType {
        //甲签
        SIGN_A(1),
        //乙签
        SIGN_B(2);
        private Integer type;

        SignType(Integer type) {
            this.type = type;
        }

        public Integer getType() {
            return type;
        }
    }

    public static void main(String[] args) {
        List<String> contents = new ArrayList<>();
        contents.add("甲方法定代表人:");
        contents.add("联系电话:");
        contents.add("身份证号码:");
        contents.add(DateUtil.format2str("yyyy 年  MM 月  dd 日"));
        String input = "/Users/laoliangliang/Downloads/合同模板 (1).pdf";
        String tempPath = "/Users/laoliangliang/Downloads/合同模板_signed.pdf";

        String filePath = "/Users/laoliangliang/Downloads/31.png";
        String fileOut = "/Users/laoliangliang/Downloads/合同模板_signed_signed_signed.pdf";
        PdfParser pdfParser = new PdfParser();
//        pdfParser.startSign(input, input, fileOut, filePath, SignType.SIGN_A, contents, false);
        pdfParser.startSign(input, fileOut, tempPath, filePath, SignType.SIGN_B, contents, true);
    }

    /**
     * 甲乙方签名方法
     *
     * @param rootPath 初始合同pdf路径
     * @param tempPath 基于哪份合同签章,比如甲方先签,这里填的就是初始合同地址;若是乙方签,这里填的就是甲方签过生成的合同地址
     * @param outPath  输出的合同地址,包含文件名
     * @param imgPath  签章图片地址
     * @param signType 甲方签章还是乙方签章,输入枚举类型
     * @param contents 签章处文本内容
     * @param already  理论上甲签的时候是false,表示没有签过,乙签的时候是true,表示甲已经签过,就算下面高度不够也不会新增页面
     *                 若需求改动,可以乙先签,那逻辑控制,先签的false,后签的true;
     *                 该项错误可能导致第二方签章时新启一页签章
     */
    public void startSign(String rootPath, String tempPath, String outPath, String imgPath, SignType signType, List<String> contents, boolean already) {
        String tempRootPath = "";
        try {
            //读取文章尾部位置
            MyRectangle myRectangle = getLastWordRectangle(rootPath);
            //还没签印的,临时文件路径
            tempRootPath = rootPath.substring(0, rootPath.length() - 4) + "_temp.pdf";
            //添加尾部内容
            SignPosition signPosition = addTailSign(myRectangle, tempPath, tempRootPath, signType.getType(), contents, already);
            InputStream in = PdfParser.class.getClassLoader().getResourceAsStream("keystore.p12");
            byte[] fileData = SignPdf.sign("123456", in, tempRootPath, imgPath, signPosition.getX(), signPosition.getY(), signPosition.getPageNum());
            FileUtil.uploadFile(fileData, outPath);
        } catch (Exception e) {
            log.error("签名出错", e);
        } finally {
            File file = new File(tempRootPath);
            if (file.exists()) {
                boolean flag = file.delete();
                if (flag) {
                    log.debug("临时文件删除成功");
                }
            }
        }
    }

    /**
     * 添加尾部签名部分(不含签名或印章)
     *
     * @param myRectangle 文档末尾位置和大致信息
     * @param input       输入文档路径
     * @param output      输出文档路径
     * @param type        1-甲签 2-乙签
     * @param content     填写内容
     * @param already     理论上甲签的时候是false,表示没有签过,乙签的时候是true,表示甲已经签过,就算下面高度不够也不会新增页面
     *                    若需求改动,可以乙先签,那逻辑控制,先签的false,后签的true
     * @throws Exception
     */
    private SignPosition addTailSign(MyRectangle myRectangle, String input, String output, Integer type, List<String> content, boolean already) throws Exception {

        PdfReader reader = new PdfReader(input);
        PdfWriter writer = new PdfWriter(output);
        PdfDocument pdf = new PdfDocument(reader, writer);
        int numberOfPages = pdf.getNumberOfPages();

        Document doc = new Document(pdf);
        String dateFontPath;
        if (sysconfig == null) {
            dateFontPath = "/Library/Fonts/simsun.ttc";
        }else{
            dateFontPath = sysconfig.getProperties().getProperty("date_font_path");
        }
        PdfFont font = PdfFontFactory.createFont(dateFontPath + ",1", PdfEncodings.IDENTITY_H, true);
        //判断签名高度是否够
        int size = content.size();
        float maxRecHeight = myRectangle.getMinlineHeight() * size;
        float v = myRectangle.getBottom() - maxRecHeight;
        boolean isNewPage = false;
        if (v <= myRectangle.getMinlineHeight() * 3) {
            isNewPage = true;
            if (!already) {
                pdf.addNewPage();
                numberOfPages++;
            }
            myRectangle.setBottom(myRectangle.getTop() * 2 - maxRecHeight * 2);
        }
        Table table = new Table(1);
        table.setPageNumber(numberOfPages);
        float bottom = (myRectangle.getBottom() - maxRecHeight) / 2;
        float left1;
        left1 = myRectangle.getLeft() + 30f;
        if (type == 2) {
            left1 = left1 + myRectangle.getWidth() / 2 - 15;
        }
        myRectangle.setLeft(left1);
        table.setFixedPosition(left1, bottom, 200);
        table.setBorder(Border.NO_BORDER);


        for (String text : content) {
            Paragraph paragraph = new Paragraph();
            paragraph.add(text).setFont(font).setFontSize(myRectangle.getHeight());
            Cell cell = new Cell();
            cell.add(paragraph);
            cell.setBorder(Border.NO_BORDER);
            table.addCell(cell);
        }

        doc.add(table);
        doc.flush();
        pdf.close();
        return getSignPosition(myRectangle, content, bottom, numberOfPages, isNewPage);
    }

    private SignPosition getSignPosition(MyRectangle myRectangle, List<String> content, float bottom, int numberOfPages, boolean isNewPage) {
        SignPosition signPosition = new SignPosition();
        //y轴位置,底部
        if (isNewPage) {
            signPosition.setY(bottom + (content.size() - 2) * myRectangle.getMinlineHeight());
        } else {
            signPosition.setY(bottom + (content.size() - 3) * myRectangle.getMinlineHeight());
        }
        //x轴位置,文字宽度+偏移量
        signPosition.setX(myRectangle.getLeft() + content.get(0).length() * myRectangle.getHeight() - 15f);
        signPosition.setPageNum(numberOfPages);
        return signPosition;
    }

    /**
     * 拿到文章末尾参数
     */
    private MyRectangle getLastWordRectangle(String input) throws IOException {
        PdfDocument pdfDocument = new PdfDocument(new PdfReader(input));
        MyEventListener myEventListener = new MyEventListener();
        PdfDocumentContentParser parser = new PdfDocumentContentParser(pdfDocument);
        parser.processContent(pdfDocument.getNumberOfPages(), myEventListener);
        List<Rectangle> rectangles = myEventListener.getRectangles();
        float left = 100000;
        float right = 0;
        float bottom = 100000;
        boolean isTop = true;
        Rectangle tempRec = null;
        float minV = 1000;
        MyRectangle myRectangle = new MyRectangle();
        //拿到文本最左最下和最右位置
        for (Rectangle rectangle : rectangles) {
            if (isTop) {
                myRectangle.setTop(rectangle.getY());
                isTop = false;
            }
            if (tempRec != null) {
                float v = tempRec.getY() - rectangle.getY();
                if (v < minV && v > 5f) {
                    minV = v;
                }
            }
            tempRec = rectangle;
            float lt = rectangle.getLeft();
            float rt = rectangle.getRight();
            float y = rectangle.getBottom();
            if (lt < left) {
                left = lt;
            }
            if (rt > right) {
                right = rt;
            }
            if (y < bottom) {
                bottom = y;
            }

        }
        Rectangle rectangle = rectangles.get(rectangles.size() - 1);
        float height = rectangle.getHeight();
        myRectangle.setHeight(height);
        myRectangle.setLeft(left);
        myRectangle.setRight(right);
        myRectangle.setBottom(bottom);
        myRectangle.setMinlineHeight(minV);
        myRectangle.setLineSpace(minV - height);
        myRectangle.setWidth(right - left);
        pdfDocument.close();
        return myRectangle;
    }


    static class MyEventListener implements IEventListener {
        private List<Rectangle> rectangles = new ArrayList<>();

        @Override
        public void eventOccurred(IEventData data, EventType type) {
            if (type == EventType.RENDER_TEXT) {
                TextRenderInfo renderInfo = (TextRenderInfo) data;
                if ("".equals(renderInfo.getText().trim())) {
                    return;
                }
                Vector startPoint = renderInfo.getDescentLine().getStartPoint();
                Vector endPoint = renderInfo.getAscentLine().getEndPoint();
                float x1 = Math.min(startPoint.get(0), endPoint.get(0));
                float x2 = Math.max(startPoint.get(0), endPoint.get(0));
                float y1 = Math.min(startPoint.get(1), endPoint.get(1));
                float y2 = Math.max(startPoint.get(1), endPoint.get(1));
                rectangles.add(new Rectangle(x1, y1, x2 - x1, y2 - y1));
            }
        }

        @Override
        public Set<EventType> getSupportedEvents() {
            return new LinkedHashSet<>(Collections.singletonList(EventType.RENDER_TEXT));
        }

        public List<Rectangle> getRectangles() {
            return rectangles;
        }

        public void clear() {
            rectangles.clear();
        }
    }

}
  1. MyRectangle 用来存文档尾部数据的实体类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zhiyis.framework.util.itext;

/**
 * @author laoliangliang
 * @date 2018/11/23 16:11
 */
public class MyRectangle {

    private float width;
    private float left;
    private float right;
    private float bottom;
    private float top;
    private float height;
    /**
     * 行间间隔
     */
    private float lineSpace;
    /**
     * 最小行间距,从上一行底部到下一行底部的距离
     */
    private float minlineHeight;
    public float getWidth() {
        return width;
    }

    public void setWidth(float width) {
        this.width = width;
    }
    public float getLeft() {
        return left;
    }

    public void setLeft(float left) {
        this.left = left;
    }

    public float getRight() {
        return right;
    }

    public void setRight(float right) {
        this.right = right;
    }

    public float getBottom() {
        return bottom;
    }

    public void setBottom(float bottom) {
        this.bottom = bottom;
    }

    public float getHeight() {
        return height;
    }

    public void setHeight(float height) {
        this.height = height;
    }

    public float getLineSpace() {
        return lineSpace;
    }

    public void setLineSpace(float lineSpace) {
        this.lineSpace = lineSpace;
    }

    public float getMinlineHeight() {
        return minlineHeight;
    }

    public void setMinlineHeight(float minlineHeight) {
        this.minlineHeight = minlineHeight;
    }

    public float getTop() {
        return top;
    }

    public void setTop(float top) {
        this.top = top;
    }
}
  1. SignPosition 签章位置类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zhiyis.framework.util.itext;

/**
 * 签章位置类
 * @author laoliangliang
 * @date 18/11/24 下午1:43
 */
public class SignPosition {

    private float x;

    private float y;

    private float width;

    private float height;

    private Integer pageNum;

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public float getX() {
        return x;
    }

    public void setX(float x) {
        this.x = x;
    }

    public float getY() {
        return y;
    }

    public void setY(float y) {
        this.y = y;
    }

    public float getWidth() {
        return width;
    }

    public void setWidth(float width) {
        this.width = width;
    }

    public float getHeight() {
        return height;
    }

    public void setHeight(float height) {
        this.height = height;
    }
}
  1. SignPdf 签章类
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zhiyis.framework.util;

import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfSignatureAppearance.RenderingMode;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.security.*;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.*;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.UUID;

/**
 * 签印
 */
public class SignPdf {
    /**
     * @param password     秘钥密码
     * @param inputStream  秘钥文件
     * @param signPdfSrc   签名的PDF文件
     * @param signImage    签名图片文件
     * @param x            x坐标
     * @param y            y坐标
     * @return
     */
    public static byte[] sign(String password, InputStream inputStream, String signPdfSrc, String signImage,
                              float x, float y,int page) {
        File signPdfSrcFile = new File(signPdfSrc);
        PdfReader reader = null;
        ByteArrayOutputStream signPDFData = null;
        PdfStamper stp = null;
        try {
            BouncyCastleProvider provider = new BouncyCastleProvider();
            Security.addProvider(provider);
            KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
            // 私钥密码 为Pkcs生成证书是的私钥密码 123456
            ks.load(inputStream, password.toCharArray());
            String alias = (String) ks.aliases().nextElement();
            PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray());
            Certificate[] chain = ks.getCertificateChain(alias);
            reader = new PdfReader(signPdfSrc);
            signPDFData = new ByteArrayOutputStream();
            // 临时pdf文件
            File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf");
            stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true);
            stp.setFullCompression();
            PdfSignatureAppearance sap = stp.getSignatureAppearance();
            sap.setReason("数字签名,不可改变");
            // 使用png格式透明图片
            Image image = Image.getInstance(signImage);
            sap.setImageScale(0);
            sap.setSignatureGraphic(image);
            sap.setRenderingMode(RenderingMode.GRAPHIC);
            int size = 120;
            // 是对应x轴和y轴坐标
            float lly = y;
            sap.setVisibleSignature(new Rectangle(x, lly, x + size, lly+size), page,
                    UUID.randomUUID().toString().replaceAll("-", ""));
            stp.getWriter().setCompressionLevel(5);
            ExternalDigest digest = new BouncyCastleDigest();
            ExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA512, provider.getName());
            MakeSignature.signDetached(sap, digest, signature, chain, null, null, null, 0, CryptoStandard.CADES);
            stp.close();
            reader.close();
            return signPDFData.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            if (signPDFData != null) {
                try {
                    signPDFData.close();
                } catch (IOException e) {
                }
            }

            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }

}
  1. 工具方法
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static boolean uploadFile(byte[] file, String filePath) throws Exception {
    String tempPath = filePath.substring(0,filePath.lastIndexOf("/"));
    File targetFile = new File(tempPath);
    if(!targetFile.exists()) {
        boolean out = targetFile.mkdirs();
        if(out) {
            log.info(filePath + " create success");
        } else {
            log.info(filePath + " create fail");
        }
    }

    FileOutputStream out1 = new FileOutputStream(filePath);
    out1.write(file);
    out1.flush();
    out1.close();
    File f = new File(filePath);
    return f.exists();
}

总结

  1. 公私钥的生成网上很多就自己去生成吧
  2. 如果想要测试效果的可以把签章部分先去掉也可以运行
  3. 我觉得这篇博客是我最有含金量的一篇了~我找了很多博客定位pdf签章的没有靠谱的,很多技术实现都很复杂,我最初版本,也就是前面有一篇博客实现就是改编自网上一篇博客的,但是有很多问题,代码也过于复杂难懂,弯弯绕绕且难以修改增强。
  4. 我研究了官方最新代码结合自己脑洞大开的思路,精简出了很简单的三个类,其实排除实体类,真正实现功能就一个PdfParser
  5. **如果觉得有用给我点个赞哦^_^**
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-11-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
python 阅读器,文字转语音—-新技能你get到了吗
我喜欢上了看小说,不知道为什么,这是一个谜,(因为我是谜一样的男人,哈哈),看着看着感觉眼皮在打架,突然我想,要是有一个人可以阅读就好了(这里我们明显感觉小编与世界脱轨),那不如写一个自动阅读的软件好了,然后就有了语音阅读神器。
全栈程序员站长
2021/09/23
8290
python 阅读器,文字转语音—-新技能你get到了吗
基于GitHub的敏捷学习方法之道与术|洞见
持续行动,持续反思,持续进步。—— via. 敏捷学习宣言 前言 对时间的敬畏 需要好多年才能懂得,最好不是去震惊世界,而是要像易卜生所说的,生活在世界上。 我们都一样,渴望着建树功勋、改变世界。可是伴随着年岁的增长,却发现梦想仍旧遥远,而时间却依然残酷的流逝着,不会仅仅因为「你」而发生丝毫的改变。如《奇特的一生》当中所言,我对时间始终充满着敬畏之心,最好的方式也不过是奢求时间能够跟自己做朋友,伴随着我这也许注定朴实无华的一生,共同成长。 在我们一生所能做的事情里,睡眠占去1/3,此生只剩2/3,除去
ThoughtWorks
2018/04/17
8640
基于GitHub的敏捷学习方法之道与术|洞见
Serverless 应用开发指南:基于 Serverless 与 Lambda 的微信公共平台
Serverless 在事件驱动方面具有天然的优势,其中之一就是聊天机器人。可要做聊天机器人不是一件容易的事,微信和 QQ 都只能用 Hack 的方式进行。
Phodal
2018/01/29
4.3K0
基于 GitHub API 的 Issue 和 PR 自动化解决方案
在开源项目中,Issue 和 Pull Request(PR)的数量庞大且管理复杂,这可能对项目的进度和质量造成负面影响。通过引入自动化工具和标准化流程,开发者可以显著优化 Issue 和 PR 的管理效率。本文将探索如何通过工具(如 GitHub Actions)和流程改进管理 Issue 和 PR 的优先级排序、自动标记和分配等功能,并提供一个基于 Python 和 GitHub API 的可运行 Demo。
Swift社区
2025/01/03
1860
基于 GitHub API 的 Issue 和 PR 自动化解决方案
巧用 Serverless,轻松搭建微信公众号的智能后台服务
一般来说,想给微信公众号增加更多的功能,需要有一台服务器,来进行公众号后台服务的搭建。那么在 Serverless 架构下,是否有更简便的方法来实现这么一个公众号后台呢?我们试试? 初步搭建 一、Serverless 原生开发 首先要有一个微信公众号! 接下来,我们要为我们的函数计算服务申请固定 IP: 点击白名单之后,我们可以填写表单,完成固定公网出口 IP 的申请。 接下来进行代码开发。 将函数绑定到公众号后台,并按照文档在函数中完成一个基本的鉴定功能: def checkSignature(
腾讯云serverless团队
2020/05/22
3.6K0
[微服务架构 】微服务简介,第1部分
每个人都在谈论微服务。行业资深人士可能会记住单片或基于SOA的解决方案是做事的方式。时代变了。新工具使开发人员能够专注于特定问题,而不会给部署或通常与隔离服务相关的其他管理任务增加过多的复杂性。选择使用合适的工具来解决正确的问题变得越来越容易。
架构师研究会
2019/09/16
7820
[微服务架构 】微服务简介,第1部分
基于Github issues + umi 搭建一个免费的带评论功能的博客(二)
上一篇文章我主要介绍了什么是Github App,以及如何利用GitHub App为我们的repository进行授权,解决了博客的数据存储和获取,那么这篇文章我将着重介绍博客搭建过程中用到的前端技术。
astonishqft
2022/05/10
5820
基于Github issues + umi 搭建一个免费的带评论功能的博客(二)
​Kubernetes 两步验证 - 使用 Serverless 实现动态准入控制
Admission 是在用户执行 kubectl 通过认证之后,在将资源持久化到 ETCD 之前的步骤,Kubernetes 为了将这部分逻辑解耦,通过调用 Webhook 的方式来实现用户自定义业务逻辑的补充。而以上过程,都是在用户执行 kuberctl 并等待 API Server 同步返回结果的生命周期内。
腾讯云 CODING
2020/07/01
1.2K0
​Kubernetes 两步验证 - 使用 Serverless 实现动态准入控制
前端为什么要关注 Serverless?
Serverless 的概念或应用场景我们以前讲过很多,这里不再冗述。概括性地讲 —— Serverless 的内涵就是对全部底层资源和运维工作的封装,让开发者更专注于业务逻辑。
Aceyclee
2020/03/20
8960
前端为什么要关注 Serverless?
使用 Serverless + 飞书打造你的个性化消息提醒系统
如果每件事都花时间去关注,那我们的时间必然会不够用,那有没有什么办法可以让这些消息集中起来并且及时推送呢?在这里我想向大家推荐一个解决方案,那就是使用 Serverless + 飞书打造属于自己的个性化消息提醒系统。
腾讯云serverless团队
2020/07/09
1.8K0
你可能不知道的15个有用的Github功能
我们平时的工作中,github是必不可少的代码托管平台,但是大多数同学也只是把它做为了托管代码的地方,并没有合理的去运用。
前端森林
2020/06/22
1.1K0
微信网页授权并获取用户信息
这一篇写的还是很清楚的,所以推荐一下,后面我会补一个Vue版本微信授权登录的例子。
前端黑板报
2018/12/26
3.1K1
基于Github issues + umi 搭建一个免费的带评论功能的博客(一)
作为一个工作了好几年的前端搬砖狗,搭建一个属于自己的博客是很有必要的,一来可以总结自己的开发学习经验,二来可以分享和记录下自己的学习轨迹,可谓好处多多,那么今天我就给大家介绍一种搭建博客的新方式。
astonishqft
2022/05/10
7250
基于Github issues + umi 搭建一个免费的带评论功能的博客(一)
Go WEB进阶实战:基于GoFrame搭建的电商前后台API系统
最近有很多小伙伴私信我:在学完Go基础后,想使用一个框架实战一个商业项目,但是又苦于不知道选择什么框架,更不知道做什么商业项目。
王中阳Go
2022/10/26
1.4K0
Go WEB进阶实战:基于GoFrame搭建的电商前后台API系统
hexo-butterfly-评论系统引入
​ 可参考官网提供的评论系统接入方式进行构建,在此过程中也陆陆续续摸索了网友们对各个评论的评价和使用的情况,可结合自身的情况进行调整,从多个方面考虑,不外乎第三方托管应用权限问题、自建服务维护/学习成本、组件引用便捷性等
hahah
2022/06/15
1.9K0
一文带你搞懂GitHub OAuth(下)
通过OAuth,第三方应用程序可以在用户授权的情况下安全地访问GitHub上的数据,而不需要获取用户的GitHub凭据。
闫同学
2024/01/15
4200
一文带你搞懂GitHub OAuth(下)
【玩转云函数】打通Github到企微的消息通知
Dear,大家好,我是“前端小鑫同学”,😇长期从事前端开发,安卓开发,热衷技术,在编程路上越走越远~ 在昨天18号的团队内部知识分享会上同事将近期为团队工程化所做的企微机器人做了详细的分享,主要是每天会有不少的时间都是在处理Merge或在找同事Merge的路上,为了优化这块的时间我们同事使用NodeJs开发服务来连接内部使用的工蜂平台和企微平台,做到自动发送和提醒对应的同事来做代码评审,当评审通过后主动通知发起人来完成合并。 那么我想做什么? &ensp;&ensp;&ensp;&ensp; 在之前我写了
前端小鑫同学
2022/12/26
1.2K0
【玩转云函数】打通Github到企微的消息通知
【玩转腾讯云】使用Serverless+飞书打造你的个性化消息提醒系统
如果每件事都花时间去关注,那我们的时间必然会不够用,那有没有什么办法可以让这些消息集中起来并且及时推送呢?在这里我想向大家推荐一个解决方案,那就是使用Serverless+飞书打造属于自己的个性化消息提醒系统。
用户1358150
2020/04/08
2.4K0
【玩转腾讯云】使用Serverless+飞书打造你的个性化消息提醒系统
小程序·云开发的HTTP API调用丨实战
通过应用生成器工具 express-generator 可以快速创建一个应用的骨架。
腾讯云开发TCB
2019/09/19
3.5K0
[- Flutter基础篇 -] 网络访问
小头像-->Settings-->Developer settings -->Personal access tokens-->Generate new token
张风捷特烈
2020/04/30
2.4K0
[- Flutter基础篇 -] 网络访问
推荐阅读
相关推荐
python 阅读器,文字转语音—-新技能你get到了吗
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档