QLExpress表达式实战—数据对比

背景

我们在流量对比的过程中,对比规则是不同的,可能需要:

  1. 全字段对比;
  2. 忽略空值对比;
  3. 排序后对比;
  4. 指定字段对比;
  5. 忽略字段对比;
  6. 字段加工对比;

如果要实现脚本对比,需要解决的难点:嵌套对象如何获取参数对应值(扁平化:xx.xx获取);

扁平化对象

public class FlowData implements Serializable {
    private static final long serialVersionUID = -8677073254096521327L;

    private String dataId;
    private final Map<String, Object> data = Maps.newHashMap();

    public FlowData() {
        this.dataId = null;
    }

    public FlowData(String dataId) {
        this.dataId = dataId;
    }
    public FlowData(FlowData flowData) {
        this.dataId = flowData.dataId;
        this.data.putAll(flowData.data);
    }
    public FlowData(String dataId, FlowData flowData) {
        this.dataId = dataId;
        this.data.putAll(flowData.data);
    }
    public String getDataId() {
        return dataId;
    }
    public void setDataId(String dataId) {
        this.dataId = dataId;
    }

    /**
     * 按照路径找到相应值
     */
    public Object getData(String fieldPath) {
        if (StringUtils.isEmpty(fieldPath)) {
            return null;
        }
        String[] splitFields = fieldPath.split("\\.");

        Object curData = data;
        for (String splitField : splitFields) {
            if (Objects.isNull(curData)) {
                return null;
            }
            if (curData instanceof Map) {
                curData = ((Map<?, ?>) curData).get(splitField);
                continue;
            }
            if (curData instanceof List && StringUtils.isNumeric(splitField)
                    && Integer.parseInt(splitField) < ((List<?>) curData).size()) {
                curData = ((List<?>) curData).get(Integer.parseInt(splitField));
                continue;
            }
            return null;
        }
        return curData;
    }

    public Map<String, Object> getData() {
        return data;
    }

    public void putData(String fieldPath, Object data) {
        Map<String, Object> updateMap = Maps.newHashMap();
        updateMap.put(fieldPath, data);
        putData(updateMap);
    }

    public void putData(Map<String, Object> updateMap) {
        updateMap(data, ParamUtils.foldData(updateMap));
    }

    public void putData(FlowData updateData) {
        updateMap(data, updateData.data);
    }

    public synchronized void removeData(String fieldPath) {
        if (StringUtils.isEmpty(fieldPath)) {
            return;
        }
        String[] splitFields = fieldPath.split("\\.");

        Object curData = data;
        for (int i = 0; i < splitFields.length - 1; i++) {
            if (Objects.isNull(curData)) {
                return;
            }
            String splitField = splitFields[i];
            if (curData instanceof Map) {
                curData = ((Map<?, ?>) curData).get(splitField);
                continue;
            }
            if (curData instanceof List && StringUtils.isNumeric(splitField)
                    && Integer.parseInt(splitField) < ((List<?>) curData).size()) {
                curData = ((List<?>) curData).get(Integer.parseInt(splitField));
                continue;
            }
            return;
        }

        String lastSplitField = splitFields[splitFields.length - 1];
        if (curData instanceof Map) {
            ((Map<?, ?>) curData).remove(lastSplitField);

        }
        if (curData instanceof List && StringUtils.isNumeric(lastSplitField)
                && Integer.parseInt(lastSplitField) < ((List<?>) curData).size()) {
            ((List<?>) curData).remove(Integer.parseInt(lastSplitField));
        }

    }

    private synchronized void updateMap(Map<String, Object> mainMap, Map<String, Object> updateMap) {
        if (Objects.isNull(mainMap)) {
            return;
        }
        for (Entry<String, Object> entry : updateMap.entrySet()) {
            String key = entry.getKey();
            Object updateValue = entry.getValue();
            if (mainMap.containsKey(key)) {
                Object mainValue = mainMap.get(key);
                if (mainValue instanceof Map && updateValue instanceof Map) {
                    updateMap((Map<String, Object>) mainValue, (Map<String, Object>) updateValue);
                    continue;
                }
                if (mainValue instanceof List && updateValue instanceof List) {
                    mainMap.put(key, updateValue);
                    continue;
                }
            }
            mainMap.put(key, updateValue);
        }
    }


    /**
     * 找到最后一个有效路径
     */
    private String getValidPath(String fieldPath) {
        if (Objects.isNull(data)) {
            return null;
        }

        List<String> result = Lists.newArrayList();
        String[] splitFields = fieldPath.split("\\.");
        Object curData = data;

        for (String field : splitFields) {
            Object nextData = null;
            if (curData instanceof Map) {
                nextData = ((Map<?, ?>) curData).get(field);
            }
            if (curData instanceof List && StringUtils.isNumeric(field)) {
                nextData = ((List<?>) curData).get(Integer.parseInt(field));
            }
            if (Objects.isNull(nextData)) {
                break;
            }
            result.add(field);
            curData = nextData;
        }
        if (CollectionUtils.isEmpty(result)) {
            return null;
        }
        return StringUtils.join(result, ".");
    }

    @Override
    public String toString() {
        return ObjectMapperUtils.toJSON(data);
    }
}

工具类:

public class ParamUtils {

    public static String convertField(String field) {
        return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, field);
    }

    public static Map<String, Object> foldData(Map<String, Object> data) {
        Map<String, Object> result = Maps.newHashMap();
        if (MapUtils.isEmpty(data)) {
            return result;
        }
        data.forEach((k, v) -> {
            String[] splitKey = k.split("\\.");
            int index = 0;
            Map<String, Object> indexMap = result;

            while (index < splitKey.length - 1) {
                String subKey = splitKey[index];
                indexMap = foldGetFromMap(indexMap, subKey);
                index++;
            }

            indexMap.put(splitKey[index], v);
        });
        return convertDataArray(result);
    }

    private static Map<String, Object> foldGetFromMap(Map<String, Object> map, String key) {
        Object subNode = map.get(key);
        if (Objects.nonNull(subNode) && subNode instanceof Map) {
            return (Map<String, Object>) subNode;
        }
        Map<String, Object> result = Maps.newHashMap();
        map.put(key, result);
        return result;
    }

    private static Map<String, Object> convertDataArray(Map<String, Object> map) {
        Map<String, Object> result = Maps.newHashMap();
        if (MapUtils.isEmpty(map)) {
            return map;
        }
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String k = entry.getKey();
            Object v = entry.getValue();
            if (!(v instanceof Map)) {
                result.put(k, v);
                continue;
            }
            Map<String, Object> valueMap = convertDataArray((Map<String, Object>) v);
            if (valueMap.keySet().stream().allMatch(StringUtils::isNumeric)) {
                result.put(k, valueMap.values());
                continue;
            }
            result.put(k, valueMap);
        }
        return result;
    }

    /**
     * 打平map结构,获取入参元数据
     *
     * @param prefix 打平的前缀
     * @param data   {"name":"tom","age":123}
     * @return 打平后的结构{"name":"string","age":"int"}
     */
    public static Map<String, String> unfoldSampleData(String prefix, Map<String, Object> data) {
        Map<String, String> result = Maps.newHashMap();
        if (MapUtils.isEmpty(data)) {
            return result;
        }
        data.forEach((k, v) -> {
            String unfoldKey;
            if (StringUtils.isNotBlank(prefix)) {
                unfoldKey = prefix + "." + k;
            } else {
                unfoldKey = k;
            }

            if (v instanceof Map) {
                result.put(unfoldKey, "MAP");
                result.putAll(unfoldSampleData(unfoldKey, (Map<String, Object>) v));
                return;
            }

            if (v instanceof Collection) {
                result.put(unfoldKey, "LIST");
                result.putAll(unfoldSampleData(unfoldKey, convertToMap((Collection) v)));
                return;
            }

            result.put(unfoldKey, v.getClass().getSimpleName());
        });
        return result;
    }

    /**
     * 打平map结构,获取入参元数据
     *
     * @param prefix 打平的前缀
     * @param data   {"name":"string","age":"int"}
     * @return 打平后的结构
     */
    public static Map<String, Object> unfoldData(String prefix, Map<String, Object> data) {
        Map<String, Object> result = Maps.newHashMap();
        if (MapUtils.isEmpty(data)) {
            return result;
        }
        data.forEach((k, v) -> {
            String unfoldKey;
            if (StringUtils.isNotBlank(prefix)) {
                unfoldKey = prefix + "." + k;
            } else {
                unfoldKey = k;
            }

            if (v instanceof Map) {
                result.put(unfoldKey, "MAP");
                result.putAll(unfoldData(unfoldKey, (Map<String, Object>) v));
                return;
            }

            if (v instanceof Collection) {
                result.put(unfoldKey, "LIST");
                result.putAll(unfoldData(unfoldKey, convertToMap((Collection) v)));
                return;
            }

            result.put(unfoldKey, v);
        });
        return result;
    }

    public static Map<String, Object> unfoldAllPathData(String prefix, Map<String, Object> data) {
        Map<String, Object> result = Maps.newHashMap();
        if (MapUtils.isEmpty(data)) {
            return result;
        }
        data.forEach((k, v) -> {
            String unfoldKey;
            if (StringUtils.isNotBlank(prefix)) {
                unfoldKey = prefix + "." + k;
            } else {
                unfoldKey = k;
            }

            if (v instanceof Map) {
                result.put(unfoldKey, v);
                result.putAll(unfoldAllPathData(unfoldKey, (Map<String, Object>) v));
                return;
            }

            if (v instanceof Collection) {
                result.put(unfoldKey, v);
                result.putAll(unfoldAllPathData(unfoldKey, convertToMap((Collection) v)));
                return;
            }

            result.put(unfoldKey, v);
        });
        return result;
    }

    private static Map<String, Object> convertToMap(Collection<?> collection) {
        Map<String, Object> result = Maps.newHashMap();
        if (CollectionUtils.isEmpty(collection)) {
            return result;
        }
        Iterator<?> iterator = collection.iterator();
        int i = 0;
        while (iterator.hasNext()) {
            result.put(String.valueOf(i), iterator.next());
            i++;
        }
        return result;

    }

    //double?
    public static String getFiledType(Object o) {
        if (Objects.isNull(o)) { //校验schema,不能有null
            throw new RuntimeException("jsonSchema配置有误,不能有null");
        }
        if (o instanceof Boolean) {
            return "Boolean";
        }
        if (o instanceof Integer || o instanceof Long) {
            return "Long";
        }
        if (o instanceof String) {
            if (StringUtils.isNumeric((String) o)) {
                return "Long";
            }
            return "String";
        }
        return null;
    }
}

表达式构造器:

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

public class ExpressBuildHandler {
    private static final Set<String> DEFAULT_KEYWORD = Sets.newHashSet("null", "true", "false");
    public static final int EXPRESS_PARAM = 1;
    public static final int EXPRESS_METHOD = 2;

    public static ExpressInfo convertExpress(String express, FlowData dataInfo) throws Exception {
        ExpressInfo expressInfo = new ExpressInfo();
        //根据字符位置,解析总的表达式,获取到方法名、参数名
        List<ExpressVar> expressVarList = getVarFromExpress(express);
        //重写表达式(替换表达式中的.为_)
        expressInfo.setExpress(replaceSeparatorForExpress(express, expressVarList));
        //构建表达式入参(将需要的入参,从流参数中解析出来)
        expressInfo.setParamMap(getParamMap(expressVarList, dataInfo));
        //构建脚本函数
        return expressInfo;
    }
    
    public static Map<String, Object> getParamMap(List<ExpressVar> expressVarList, FlowData dataInfo) {
        Map<String, Object> paramMap = Maps.newHashMap();
        for (ExpressVar entry : expressVarList) {
            if (entry.getType() == EXPRESS_PARAM) {
                String valKey = replaceSeparatorForKey(entry.getContext());
                if (!DEFAULT_KEYWORD.contains(valKey)) {
                    paramMap.put(valKey, getExpressReplaceValue(entry.getContext(), dataInfo));
                }
            }
        }
        return paramMap;
    }

    /**
     * 重新表达式,替换分隔符
     */
    private static String replaceSeparatorForExpress(String express, List<ExpressVar> expressVarList) {
        int appendStart = 0;
        StringBuilder resultBuilder = new StringBuilder();
        //处理表达式
        for (ExpressVar entry : expressVarList) {
            //替换
            if (entry.getType() == EXPRESS_PARAM) {
                String valKey = replaceSeparatorForKey(entry.getContext());
                Integer start = entry.getStart();
                resultBuilder.append(express, appendStart, start);
                resultBuilder.append(valKey);
                appendStart = entry.getEnd();
            }
        }
        if (appendStart < express.length()) {
            resultBuilder.append(express, appendStart, express.length());
        }
        return resultBuilder.toString();
    }

    private static String replaceSeparatorForKey(String paramFieldName) {
        return paramFieldName.replace(".", "_");
    }



    private static Object getExpressReplaceValue(String field, FlowData dataInfo) {
        String stringField = field.trim();
        if (DEFAULT_KEYWORD.contains(stringField)) {
            return stringField;
        }
        Object result = dataInfo.getData(stringField);
        if (Objects.isNull(result)) {
            return null;
        }
        return result;
    }

    private static List<ExpressVar> getVarFromExpress(String express) {
        List<ExpressVar> result = Lists.newArrayList();
        if (StringUtils.isBlank(express)) {
            return result;
        }
        boolean strScope = false;
        boolean varScope = false;
        int varStart = 0;
        int varEnd = 0;
        char currentChar;
        char preChar = 0;
        char[] charList = express.toCharArray();
        for (int i = 0; i < charList.length; i++) {
            currentChar = charList[i];
            if (i - 1 >= 0) {
                preChar = charList[i - 1];
            }
            //遇到引号变换字符串scope
            if (currentChar == '"' && preChar != '\\') {
                strScope = !strScope;
            }
            //非字符串且非变量scope,遇到字母进入变量scope
            if (!strScope && !varScope && Character.isLetter(currentChar)) {
                varScope = true;
                varStart = i;
                varEnd = i + 1;
                continue;
            }
            //当前处于字符串scope,进行累加或结束
            if (varScope) {
                if (Character.isLetterOrDigit(currentChar) || currentChar == '_' || currentChar == '.') {
                    varEnd++;
                    continue;
                }
                varScope = false;
                if (currentChar == '(') {
                    result.add(new ExpressVar(varStart, varEnd, 2, express.substring(varStart, varEnd)));
                } else {
                    result.add(new ExpressVar(varStart, varEnd, 1, express.substring(varStart, varEnd)));
                }
            }
        }
        if (strScope) {
            throw new RuntimeException("字符串不完整");
        }
        if (varScope) {
            result.add(new ExpressVar(varStart, varEnd, 1, express.substring(varStart, varEnd)));
        }
        return result;
    }

    @Getter
    @Setter
    @AllArgsConstructor
    public static class ExpressVar {
        private Integer start;
        private Integer end;
        private Integer type;
        private String context;
    }

    @Getter
    @Builder
    @ToString
    public static class ExpressFunction {
        private String function;
        private Object classInstance;
        private String methodName;
        private Class<?>[] paramTypes;
        private String udfFunctionImpl;
    }

    @Getter
    @Setter
    public static class ExpressInfo {
        private String express;
        private Map<String, Object> paramMap;
        private Map<String, ExpressFunction> functionMap;
    }
}

表达式执行器


import java.util.Collection;

import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;

public class ExpressExecuteHandler {

    private static final ExpressExecuteHandler INSTANCE = new ExpressExecuteHandler();
    public static final ExpressRunner RUNNER = new ExpressRunner();

    static {
        try {
            //序列化对比
            RUNNER.addFunctionOfClassMethod("toJson", InnerMethod.class.getName(), "json", new Class<?>[] {Object.class}, null);
            //有序序列化对比
            RUNNER.addFunctionOfClassMethod("toJsonIgnoreNull", InnerMethod.class.getName(), "jsonIgnoreNull", new Class<?>[] {Object.class}, null);
            //集合有序
            RUNNER.addFunctionOfClassMethod("sort", InnerMethod.class.getName(), "sort", new Class<?>[] {Collection.class}, null);
            RUNNER.addFunctionOfClassMethod("sortEx", InnerMethod.class.getName(), "sortEx", new Class<?>[] {Collection.class, String.class}, null);
            //忽略某些字段
            RUNNER.addFunctionOfClassMethod("ignoreList", InnerMethod.class.getName(), "ignoreList",
                    new Class<?>[] {Collection.class, String[].class}, null);
            //命中某些字段
            RUNNER.addFunctionOfClassMethod("hitList", InnerMethod.class.getName(), "hitList",
                    new Class<?>[] {Collection.class, String[].class}, null);
            //获取集合长度
            RUNNER.addFunctionOfClassMethod("size", InnerMethod.class.getName(), "size", new Class<?>[] {Object.class}, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public static ExpressExecuteHandler getInstance() {
        return INSTANCE;
    }

    /**
     * 执行脚本语言
     *
     * @param express        脚本表达式
     * @param defaultContext 环境变量(参数)
     */
    private Object execute(String express, DefaultContext<String, Object> defaultContext) throws Exception {
        return RUNNER.execute(express, defaultContext, null, true, false);
    }

    /**
     * 执行函数
     */
    public Object calculateForResult(ExpressBuildHandler.ExpressInfo expressInfo) throws Exception {
        StringBuilder finalExpress = new StringBuilder();
        finalExpress.append(expressInfo.getExpress());
        DefaultContext<String, Object> defaultContext = new DefaultContext<>();
        defaultContext.putAll(expressInfo.getParamMap());
        return this.execute(finalExpress.toString(), defaultContext);
    }
}

内置函数

@Slf4j
public class InnerMethod {

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    }

    //toJson策略
    public static String json(Object obj) {
        return ObjectMapperUtils.toJSON(obj);
    }

    public static int size(Object obj) {
        if (obj == null) {
            return 0;
        } else if (obj.getClass().isArray()) {
            return ((Object[]) obj).length;
        } else if (obj instanceof Collection) {
            return ((Collection) obj).size();
        }
        return 1;
    }


    //忽略null-序列化
    public static String jsonIgnoreNull(Object obj) {
        try {
            //处理单集合场景
            if (obj instanceof Collection) {
                List arr = (List) obj;
                obj = arr.stream()
                        .filter(r -> !Objects.isNull(r))
                        .filter(r -> r instanceof String ? StringUtils.isNotEmpty((String) r) : true)
                        .collect(Collectors.toList());
            }
            if (obj.getClass().isArray()) {
                Object[] arr = (Object[]) obj;
                obj = Arrays.stream(arr)
                        .filter(r -> !Objects.isNull(r))
                        .filter(r -> r instanceof String ? StringUtils.isNotEmpty((String) r) : true)
                        .collect(Collectors.toList());
            }
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    //基础集合的排序
    public static List sort(Collection<Object> targets) {
        if (CollectionUtils.isEmpty(targets)) {
            return new ArrayList();
        }
        List<Comparable> collect = targets.stream().map(r -> (Comparable) r).collect(Collectors.toList());
        return collect.stream().sorted().collect(Collectors.toList());
    }

    //普通对象的排序
    public static List sortEx(Collection<Object> targets, String sortField) {
        log.info("targets:{}", targets);
        if (CollectionUtils.isEmpty(targets)) {
            return new ArrayList<>();
        }
        Collection<Map> maps = ObjectMapperUtils.fromJSON(ObjectMapperUtils.toJSON(targets), List.class, Map.class);
        String field = sortField.replace("_", ".");
        return maps.stream().sorted(Comparator.comparing(r -> {
            FlowData flowData = new FlowData();
            flowData.putData(r);
            return String.valueOf(flowData.getData(field));
        })).collect(Collectors.toList());
    }


    public static List ignoreList(Collection<Object> targets, String[] params) {
        if (CollectionUtils.isEmpty(targets)) {
            return Lists.newArrayList();
        }
        Collection<Map> maps = ObjectMapperUtils.fromJSON(ObjectMapperUtils.toJSON(targets), List.class, Map.class);
        return maps.stream().map(r -> {
            FlowData flowData = new FlowData();
            if (r instanceof Map) {
                flowData.putData(r);
            } else {
                flowData.putData(ObjectMapperUtils.value(r, Map.class));
            }
            for (int i = 0; i < params.length; i++) {
                String param = params[i].replace("_", ".");
                flowData.removeData(param);
            }
            return flowData.getData();
        }).collect(Collectors.toList());
    }


    public static List hitList(Collection<Object> targets, String[] params) {
        if (CollectionUtils.isEmpty(targets)) {
            return Lists.newArrayList();
        }
        Collection<Map> maps = ObjectMapperUtils.fromJSON(ObjectMapperUtils.toJSON(targets), List.class, Map.class);
        return maps.stream().map(r -> {
            FlowData flowData = new FlowData();
            if (r instanceof Map) {
                flowData.putData(r);
            } else {
                flowData.putData(ObjectMapperUtils.value(r, Map.class));
            }
            FlowData targetData = new FlowData();
            for (int i = 0; i < params.length; i++) {
                String param = params[i].replace("_", ".");
                targetData.putData(param, flowData.getData(param));
            }
            return targetData.getData();
        }).collect(Collectors.toList());
    }
}

测试使用

   public static void main(String[] args) throws Exception {
        List<Address> d1 = Lists.newArrayList();
        d1.add(Address.builder().province(Province.builder().id(4L).name(null).build()).city("洛阳").area("白帝小区").phone("13617172120")
                .build());
        d1.add(Address.builder().province(Province.builder().id(2L).name("河北").build()).city("邯郸").area("邯郸小区").phone("19917172122")
                .build());
        d1.add(Address.builder().province(Province.builder().id(3L).name("山东").build()).city("德州").area("德州小区").phone("18817172121")
                .build());


        FlowData flowData = new FlowData();
        flowData.putData("this", d1);

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

推荐阅读更多精彩内容