OFD数电票解析JAVA实现

1、背景

这两年是在一家税务公司,业务主要与发票相关联,其中涉及本地发票文件的OCR识别与不同格式文件内容提取,我这边负责了OFD本地文件提取的实现,使用相对应依赖解析数据后其实是对xml数据的提取过程,以下是ofd文件说明:

OFD(Open Fixed-layout Documents的简称,意为开放版式文件)版式文档是版面呈现效果高度精确固定的电子文件,其呈现与设备无关。与pdf文件相仿,具有格式独立、版面固定、固化呈现等特点。可以说OFD是中国版的PDF,但是在很多方面的性能优于PDF的同类文档。OFD也逐渐开始在电子发票、电子公文、电子证照等等的领域中应用。

2、示例

定义一个发票提取器InvExtractor

/**
 * @author wenx
 * @description 定义xml发票解析提取器
 */
public interface InvExtractor {

    /**
     * 解析文件返回 root document
     *
     * @param file
     * @return
     */
    Element extract(File file);

    /**
     * document返回 系统发票信息map
     *
     * @param root
     * @param file
     * @return linkedHashMap
     */
    Map<String, Object> covertInvMap(Element root, File file);

}

实现提取器

/**
 * 全电ofd 提取器
 *
 * @author wenx
 */
@Slf4j
public class OfdElecExtractor implements InvExtractor {

    //全电压缩文件entrys key
    private String[] entrys = {
            "Doc_0/Annots/Page_0/Annotation.xml",
            "Doc_0/DocumentRes.xml",
            "Doc_0/PublicRes.xml",
            "Doc_0/Pages/Page_0/Content.xml",
            "Doc_0/Tpls/Tpl_0/Content.xml",
            "Doc_0/Document.xml",
            "Doc_0/Tags/CustomTags.xml",
            "Doc_0/Tags/CustomTag.xml",
            "OFD.xml",
            "Doc_0/Res/image_57.png",
            "Doc_0/Annots/Annotations.xml",
            "Doc_0/Res/image_101.png",
    };

    private static Map<String, String> typeMaps = new HashMap<>();

    //票面符号为 中文字符
    static {
        typeMaps.put("FullEleSpecInvoice", "电子发票(增值税专用发票)");
        typeMaps.put("FullEleGenerInvoice", "电子发票(普通发票)");
    }

    @SneakyThrows
    @Override
    public Element extract(File file) {
        String body;
        try (ZipFile zipFile = new ZipFile(file)) {
            ZipEntry entry = zipFile.getEntry(entrys[3]);
            InputStream input = zipFile.getInputStream(entry);
            body = StreamUtils.copyToString(input, StandardCharsets.UTF_8);
        }
//        log.info("params body:{}",body);
        Document document = DocumentHelper.parseText(body);
        return document.getRootElement();
    }

    @Override
    public Map<String, Object> covertInvMap(Element root, File file) {
        //key map
        HashMap<String, Object> keyMap = renderKeyMap(file);
        log.info("keyMap :{}", keyMap);

        //value list
        Element content = root.element("Content");
        List<Element> layers = content.elements("Layer");
        List<Element> valueList = new ArrayList<>();
        layers.forEach(l -> {
            List<Element> objs = l.elements("TextObject");
            if (!CollectionUtils.isEmpty(objs)) {
                valueList.addAll(objs);
            }

        });

        //票面信息
        LinkedHashMap<String, Object> inv = new LinkedHashMap<>();
        valueList.stream()
                .filter(o -> keyMap.get("InvoiceNo").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("eInvoiceNo", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("IssueDate").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("invoiceTime", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("BuyerName").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("buyerName", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("BuyerTaxID").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("buyerTaxNo", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("SellerName").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("sellerName", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("SellerTaxID").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("sellerTaxNo", i.elementTextTrim("TextCode")));
        valueList.stream()
                .filter(o -> keyMap.get("InvoiceClerk").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("payer", i.elementTextTrim("TextCode")));
        if(keyMap.get("Note") != null){
        valueList.stream()
                .filter(o -> keyMap.get("Note").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> inv.put("remark", i.elementTextTrim("TextCode")));
        }

        //金额
        valueList.stream()
                .filter(o -> keyMap.get("TaxExclusiveTotalAmount").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> {
                    if (!ObjectUtils.isEmpty(i.elementTextTrim("TextCode")))
                        inv.put("amount", new BigDecimal(i.elementTextTrim("TextCode")));
                });
        valueList.stream()
                .filter(o -> keyMap.get("TaxTotalAmount").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> {
                    if (!ObjectUtils.isEmpty(i.elementTextTrim("TextCode")))
                        inv.put("taxAmount", new BigDecimal(i.elementTextTrim("TextCode")));
                });
        valueList.stream()
                .filter(o -> keyMap.get("TaxInclusiveTotalAmount").equals(o.attribute("ID").getValue()))
                .findAny().ifPresent(i -> {
                    if (!ObjectUtils.isEmpty(i.elementTextTrim("TextCode")))
                        inv.put("sumAmount", new BigDecimal(i.elementTextTrim("TextCode")));
                });
        //明细
        List<LinkedHashMap<String, Object>> detailList = new ArrayList<>();
        JSONArray Items = JSON.parseArray(JSON.toJSONString(keyMap.get("Item")));
        JSONArray Specifications = JSON.parseArray(JSON.toJSONString(keyMap.get("Specification")));
        JSONArray MeasurementDimensions = JSON.parseArray(JSON.toJSONString(keyMap.get("MeasurementDimension")));
        JSONArray Amounts = JSON.parseArray(JSON.toJSONString(keyMap.get("Amount")));
        JSONArray Prices = JSON.parseArray(JSON.toJSONString(keyMap.get("Price")));
        JSONArray TaxAmounts = JSON.parseArray(JSON.toJSONString(keyMap.get("TaxAmount")));
        JSONArray TaxSchemes = JSON.parseArray(JSON.toJSONString(keyMap.get("TaxScheme")));
        JSONArray Quantitys = JSON.parseArray(JSON.toJSONString(keyMap.get("Quantity")));

        for (int i = 0; i < Items.size(); i++) {
            LinkedHashMap<String, Object> detail = new LinkedHashMap<>();
            int index = i;
            if(!ObjectUtils.isEmpty(Items)) {
                if (ObjectUtils.isEmpty(Items.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> Items.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("goodName", o.elementTextTrim("TextCode")));
            }
            if (!ObjectUtils.isEmpty(Specifications)) {
                if (ObjectUtils.isEmpty(Specifications.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> Specifications.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("general", o.elementTextTrim("TextCode")));
            }
            if(!ObjectUtils.isEmpty(MeasurementDimensions)) {
                if (ObjectUtils.isEmpty(MeasurementDimensions.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> MeasurementDimensions.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("specifications", o.elementTextTrim("TextCode")));
            }
            if(!ObjectUtils.isEmpty(Amounts)) {
                if (ObjectUtils.isEmpty(Amounts.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> Amounts.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("amount", new BigDecimal(o.elementTextTrim("TextCode"))));
            }
            if (!ObjectUtils.isEmpty(Prices)) {
                if (ObjectUtils.isEmpty(Prices.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> Prices.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("priceIncludeTax", new BigDecimal(o.elementTextTrim("TextCode"))));
            }
            if (!ObjectUtils.isEmpty(TaxAmounts)) {
                if (ObjectUtils.isEmpty(TaxAmounts.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> TaxAmounts.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("taxAmount", new BigDecimal(o.elementTextTrim("TextCode"))));
            }
            if (!ObjectUtils.isEmpty(Quantitys)) {
                if (ObjectUtils.isEmpty(Quantitys.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> Quantitys.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("goodNum", new BigDecimal(o.elementTextTrim("TextCode"))));
            }
            if (!ObjectUtils.isEmpty(TaxSchemes)) {
                if (ObjectUtils.isEmpty(TaxSchemes.get(index))) {
                    throw new BizException("商品验错误");
                }
                valueList.stream()
                    .filter(o -> TaxSchemes.get(index).equals(o.attribute("ID").getValue()))
                    .findAny().ifPresent(o -> detail.put("taxRate", new BigDecimal(o.elementTextTrim("TextCode")
                        .replace("%", "")).divide(new BigDecimal(100), 8, RoundingMode.HALF_UP)));
            }

            detailList.add(detail);
        }
        inv.put("details", detailList);

        //发票抬头
        this.extractTitle(inv, file);

        return inv;
    }

    @SneakyThrows
    private HashMap<String, Object> renderKeyMap(File file) {
        String body;
        HashMap<String, Object> keyMap = new HashMap<>();
        try (ZipFile zipFile = new ZipFile(file)) {
            ZipEntry entry = zipFile.getEntry(entrys[7]);
            InputStream input = zipFile.getInputStream(entry);
            body = StreamUtils.copyToString(input, StandardCharsets.UTF_8);
            if (StringUtils.isNotBlank(body))
                //空命名空间 转换异常 问题处理
                body = body.replaceAll("<:eInvoice xmlns:=\"\">", "<:eInvoice xmlns:ofd=\"http://www.ofdspec.org/2016\" DocType=\"OFD\" Version=\"1.1\">");
//            log.info("KeyMap body:{}",body);
        }
        Document document = DocumentHelper.parseText(body);
        Element root = document.getRootElement();

        Element Note = root.element("Note");
        if (!ObjectUtils.isEmpty(Note)) {
            keyMap.put("Note", Note.elementTextTrim("ObjectRef"));
        }
        Element IssueDate = root.element("IssueDate");
        if (!ObjectUtils.isEmpty(IssueDate)) {
            keyMap.put("IssueDate", IssueDate.elementTextTrim("ObjectRef"));
        }
        Element invoiceNo = root.element("InvoiceNo");
        if (!ObjectUtils.isEmpty(invoiceNo)) {
            keyMap.put("InvoiceNo", invoiceNo.elementTextTrim("ObjectRef"));
        }
        //buyer
        Element Buyer = root.element("Buyer");
        Element BuyerName = Buyer.element("BuyerName");
        if (!ObjectUtils.isEmpty(BuyerName)) {
            keyMap.put("BuyerName", BuyerName.elementTextTrim("ObjectRef"));
        }
        Element BuyerTaxID = Buyer.element("BuyerTaxID");
        if (!ObjectUtils.isEmpty(BuyerTaxID)) {
            keyMap.put("BuyerTaxID", BuyerTaxID.elementTextTrim("ObjectRef"));
        }
        //seller
        Element Seller = root.element("Seller");
        Element SellerName = Seller.element("SellerName");
        if (!ObjectUtils.isEmpty(SellerName)) {
            keyMap.put("SellerName", SellerName.elementTextTrim("ObjectRef"));
        }
        Element SellerTaxID = Seller.element("SellerTaxID");
        if (!ObjectUtils.isEmpty(SellerTaxID)) {
            keyMap.put("SellerTaxID", SellerTaxID.elementTextTrim("ObjectRef"));
        }
        Element TaxExclusiveTotalAmount = root.element("TaxExclusiveTotalAmount");
        //固定长度 为2 0:¥ 1:金额
        List<Element> amounts = TaxExclusiveTotalAmount.elements("ObjectRef");
        if (amounts.size() < 2) {
            throw new BizException("ofd 未税金额解析错误");
        }
        keyMap.put("TaxExclusiveTotalAmount", amounts.get(1).getText());
        //固定长度 为2 0:¥ 1:金额
        Element TaxTotalAmount = root.element("TaxTotalAmount");
        List<Element> taxAmounts = TaxTotalAmount.elements("ObjectRef");
        if (taxAmounts.size() < 2) {
            throw new BizException("ofd 税额解析错误");
        }
        keyMap.put("TaxTotalAmount", taxAmounts.get(1).getText());
        //固定长度 为2 0:¥ 1:金额
        Element TaxInclusiveTotalAmount = root.element("TaxInclusiveTotalAmount");
        List<Element> sumAmounts = TaxInclusiveTotalAmount.elements("ObjectRef");
        if (sumAmounts.size() < 2) {
            throw new BizException("ofd 含税金额解析错误");
        }
        keyMap.put("TaxInclusiveTotalAmount", sumAmounts.get(1).getText());
        List<Element> InvoiceClerks = root.elements("InvoiceClerk");
        if (CollectionUtils.isEmpty(InvoiceClerks)) {
            throw new BizException("ofd 开票人解析错误");
        }
        keyMap.put("InvoiceClerk", InvoiceClerks.get(0).elementTextTrim("ObjectRef"));

        //明细
        List<Element> Items = root.elements("Item");
        if (!CollectionUtils.isEmpty(Items)) {
            keyMap.put("Item", Items.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> Specifications = root.elements("Specification");
        if (!CollectionUtils.isEmpty(Specifications)) {
            keyMap.put("Specification", Specifications.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> TaxSchemes = root.elements("TaxScheme");
        if (!CollectionUtils.isEmpty(TaxSchemes)) {
            keyMap.put("TaxScheme", TaxSchemes.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> MeasurementDimensions = root.elements("MeasurementDimension");
        if (!CollectionUtils.isEmpty(MeasurementDimensions)) {
            keyMap.put("MeasurementDimension", MeasurementDimensions.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> Amounts = root.elements("Amount");
        if (!CollectionUtils.isEmpty(Amounts)) {
            keyMap.put("Amount", Amounts.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> TaxAmounts = root.elements("TaxAmount");
        if (!CollectionUtils.isEmpty(TaxAmounts)) {
            keyMap.put("TaxAmount", TaxAmounts.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> Prices = root.elements("Price");
        if (!CollectionUtils.isEmpty(Prices)) {
            keyMap.put("Price", Prices.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        List<Element> Quantitys = root.elements("Quantity");
        if (!CollectionUtils.isEmpty(Quantitys)) {
            keyMap.put("Quantity", Quantitys.stream()
                    .map(e -> e.elementTextTrim("ObjectRef"))
                    .collect(Collectors.toList()));
        }
        return keyMap;
    }


    @SneakyThrows
    private void extractTitle(LinkedHashMap<String, Object> inv, File file) {
        String content;
        try (ZipFile zipFile = new ZipFile(file)) {
            ZipEntry entry = zipFile.getEntry(entrys[4]);
            InputStream input = zipFile.getInputStream(entry);
            content = StreamUtils.copyToString(input, StandardCharsets.UTF_8);
        }
        Document document = DocumentHelper.parseText(content);
        Element root = document.getRootElement();
        Element c = root.element("Content");
        List<Element> lays = c.elements("Layer");
        //不确定 发票抬头 在哪个textObject 循环取值
        List<String> texts = new ArrayList<>();
        List<Element> elements = new ArrayList<>();
        lays.forEach(l -> {
            List<Element> es = l.elements("TextObject");
            if (!CollectionUtils.isEmpty(es)) {
                elements.addAll(es);
            }
        });
        elements.stream()
                .filter(e -> StringUtils.isNotBlank(e.elementTextTrim("TextCode"))
                        && e.elementTextTrim("TextCode").contains("发票"))
                .forEach(e -> texts.add(e.elementTextTrim("TextCode")));

        if (CollectionUtils.isEmpty(texts)) {
            throw new BizException("发票抬头 提取错误");
        }
        AtomicReference<String> invType = new AtomicReference<>();
        for (String text : texts) {
            typeMaps.entrySet().stream().filter(e -> text.equals(e.getValue())).findAny()
                    .ifPresent(e -> invType.set(e.getKey()));
            if (!ObjectUtils.isEmpty(invType)) {
                break;
            }
        }
        if (StringUtils.isBlank(invType.get())) {
            throw new BizException("发票抬头 转换错误");
        }
        inv.put("invoiceType", invType);

    }
}


定义提取器处理chain

public interface ExtractorHandlerChain {

    /**
     * 通过文件提取发票信息
     *
     * @param file
     * @return
     */
    Invoice process(File file);

}

实现chain提取器


/**
 * @author wenx
 * @description 收票提取为 invoice chain
 */
@Component
public class ExtractorHandlerChainDefault implements ExtractorHandlerChain {

    @Override
    public Invoice process(@NotNull File file) {
        String name = before(file);
        InvExtractor extractor = createExtractor(name);
        if (ObjectUtil.isEmpty(extractor)) {
            throw new BizException("文件提取器获取失败");
        }
        InvExtractorMapHandler mapHandler = createMapHandler(name);
        if (ObjectUtil.isEmpty(mapHandler)) {
            throw new BizException("文件提取映射获取失败");
        }
        Element element = extractor.extract(file);
        Map<String, Object> invMap = extractor.covertInvMap(element, file);
        if (ObjectUtil.isEmpty(invMap)) {
            throw new BizException("准发票数据获取失败");
        }
        Invoice invoice = mapHandler.handleInvMap(invMap);
        if (ObjectUtil.isEmpty(invoice)) {
            throw new BizException("发票信息转换获取失败");
        }
        return invoice;
    }

    @SneakyThrows
    public String before(@NotNull File file) {
        try (ZipFile zipFile = new ZipFile(file)) {
            //税控验证
            ZipEntry entry = zipFile.getEntry("Doc_0/Attachs/original_invoice.xml");
            ZipEntry eEntry = zipFile.getEntry("Doc_0/Pages/Page_0/Content.xml");
            //全电更改后缀 内容不变
            if (ObjectUtil.isEmpty(entry) && ObjectUtil.isNotEmpty(eEntry)) {
                return ELECTRON_OFD;
            }
        }
        return TAX_ODF;
    }

}

提取器静态工厂


/**
 * @author wenx
 * @description 提取器构造工厂
 */
public class ConstructExtractorFactory {

    /**
     * extractorMap 三元组
     */
    private static final Table<String, String, String> extractorMapTable = HashBasedTable.create();

    static {
        extractorMapTable.put(ELECTRON_OFD,
                "com.extractor.OfdElecExtractor",
                "com.extractor.handler.OfdElecInvExtractorMapHandler");

    }


    /**
     * 构造提取器
     *
     * @param name
     * @return
     */
    @SneakyThrows
    public static InvExtractor createExtractor(String name) {
        StringBuilder clz = new StringBuilder();
        for (String r : extractorMapTable.rowKeySet()) {
            if (r.contains(name) && !extractorMapTable.row(r).entrySet().isEmpty()) {
                extractorMapTable.row(r).keySet()
                        .stream()
                        .findAny().ifPresent(clz::append);
                break;
            }
        }
        if (CharSequenceUtil.isBlank(clz)) {
            throw new BizException("获取提取器clz失败");
        }
        return (InvExtractor) Class.forName(clz.toString()).newInstance();
    }

    /**
     * 构造处理器
     *
     * @param name
     * @return
     */
    @SneakyThrows
    public static InvExtractorMapHandler createMapHandler(String name) {
        StringBuilder clz = new StringBuilder();
        for (String r : extractorMapTable.rowKeySet()) {
            if (r.contains(name) && !extractorMapTable.row(r).entrySet().isEmpty()) {
                extractorMapTable.row(r).values()
                        .stream()
                        .findAny().ifPresent(clz::append);
                break;
            }
        }
        if (CharSequenceUtil.isBlank(clz)) {
            throw new BizException("获取转换器clz失败");
        }
        return (InvExtractorMapHandler) Class.forName(clz.toString()).newInstance();
    }


}

测试代码

   @Test
    public void testHandlerChain() {
        File file = new File("test.ofd");
        Invoice invoice = handlerChain.process(file);
        log.info("invoice:{}", invoice);
        Assertions.assertNotNull(invoice);
    }

-end-

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

推荐阅读更多精彩内容