Mybatis之通用Mapper(基于mybatis的Provider机制无需第三方插件包)

前言

几乎每个系统都需要单表的基础操作(即增删改查分页查询等),如果不使用通用的Mapper则需要每个mapper中都需要实现对应的重复方法,虽然mybatis逆向生成工具会生成对应的.xml文件。里面已经含有一些通用的方法,但是每个实体对应一个.xml文件太复杂。而mybatis也支持注解方式实现sql,使用注解方式实现sql方式,个人感觉更简洁,也符合减少配置文件的趋势。如springboot都在简化配置文件。

通用Mapper

通用Mapper就是为了解决单表增删改查,基于Mybatis Provider机制实现。开发人员不需要编写SQL,不需要在DAO中增加方法,不需要引入其他多余的第三方框架。只要写好实体类,就能支持相应的增删改查方法。

mybatis注解方式实现sql编写

/**
*注解方式实现sql
*/
public interface UserMapper{
    @Select("SELECT id, name FROM tb_user WHERE user_id=#{userId}")
    User selectById(Integer userId);
}

以上就是通过mybatis注解方式实现sql语句调用,是不是看上去就显得更简洁。

BaseMapper代码如下:

package mayfly.core.base.mapper;


import mayfly.core.base.mapper.annotation.NoColumn;
import mayfly.core.base.mapper.annotation.PrimaryKey;
import mayfly.core.base.mapper.annotation.Table;
import mayfly.core.util.CollectionUtils;
import mayfly.core.util.PlaceholderResolver;
import mayfly.core.util.ReflectionUtils;
import mayfly.core.util.StringUtils;
import org.apache.ibatis.annotations.DeleteProvider;
import org.apache.ibatis.annotations.InsertProvider;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider;
import org.apache.ibatis.builder.annotation.ProviderContext;
import org.apache.ibatis.jdbc.SQL;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

/**
 * 通用Mapper,实现基本功能
 *
 * @author meilin.huang
 * @param <I>  主键类型
 * @param <E>  实体类型
 */
public interface BaseMapper<I, E> {

    /**
     * 插入新对象,并返回主键id值(id通过实体获取)
     *
     * @param entity 实体对象
     * @return  影响条数
     */
    @InsertProvider(type = InsertSqlProvider.class, method = "sql")
    @Options(useGeneratedKeys = true, keyColumn = TableInfo.DEFAULT_PRIMARY_KEY)
    int insert(E entity);

    /**
     * 插入新对象(只设置非空字段),并返回主键id值(id通过实体获取)
     *
     * @param entity 实体对象
     * @return  影响条数
     */
    @InsertProvider(type = InsertSelectiveSqlProvider.class, method = "sql")
    @Options(useGeneratedKeys = true, keyColumn = TableInfo.DEFAULT_PRIMARY_KEY)
    int insertSelective(E entity);

    /**
     * 批量插入实体
     *
     * @param entities  实体列表
     * @return          影响条数
     */
    @InsertProvider(type = BatchInsertSqlProvider.class, method = "sql")
    int batchInsert(@Param("entities") List<E> entities);

    /**
     * 根据主键id更新实体,若实体field为null,则对应数据库的字段也更新为null
     *
     * @param entity  实体对象
     * @return         影响条数
     */
    @UpdateProvider(type = UpdateSqlProvider.class, method = "sql")
    int updateByPrimaryKey(E entity);

    /**
     * 根据主键id更新实体,若实体field为null,则对应数据库的字段不更新
     *
     * @param entity  实体对象
     * @return        影响条数
     */
    @UpdateProvider(type = UpdateSelectiveSqlProvider.class, method = "sql")
    int updateByPrimaryKeySelective(E entity);

    /**
     * 根据主键id删除
     *
     * @param id  id
     * @return  影响条数
     */
    @DeleteProvider(type = DeleteSqlProvider.class, method = "sql")
    int deleteByPrimaryKey(I id);

    /**
     * 伪删除,即将is_deleted字段更新为1
     *
     * @param id id
     * @return  影响条数
     */
    @UpdateProvider(type = FakeDeleteSqlProvider.class, method = "sql")
    int fakeDeleteByPrimaryKey(I id);

    /**
     * 根据实体条件删除
     *
     * @param criteria  实体
     * @return  影响条数
     */
    @DeleteProvider(type = DeleteByCriteriaSqlProvider.class, method = "sql")
    int deleteByCriteria(E criteria);

    /**
     * 根据id查询实体
     *
     * @param id  id
     * @return    实体
     */
    @SelectProvider(type = SelectOneSqlProvider.class, method = "sql")
    E selectByPrimaryKey(I id);

    /**
     * 查询所有实体
     *
     * @param orderBy  排序
     * @return   实体list
     */
    @SelectProvider(type = SelectAllSqlProvider.class, method = "sql")
    List<E> selectAll(String orderBy);

    /**
     * 根据id列表查询实体列表
     * @param ids  id列表
     * @return  list
     */
    @SelectProvider(type = SelectByPrimaryKeyInSqlProvider.class, method = "sql")
    List<E> selectByPrimaryKeyIn(@Param("ids") List<I> ids);

    /**
     * 根据实体条件查询符合条件的实体list
     * @param criteria  条件实体
     * @return          list
     */
    @SelectProvider(type = SelectByCriteriaSqlProvider.class, method = "sql")
    List<E> selectByCriteria(E criteria);

    /**
     * 根据条件查询单个数据
     *
     * @param criteria  实体条件
     * @return          实体对象
     */
    @SelectProvider(type = SelectByCriteriaSqlProvider.class, method = "sql")
    E selectOneByCriteria(E criteria);

    /**
     * 返回实体总数
     *
     * @return  总数
     */
    @SelectProvider(type = CountSqlProvider.class, method = "sql")
    long count();

    /**
     * 根据条件查询符合条件的实体总数
     *
     * @param criteria  实体条件
     * @return    数量
     */
    @SelectProvider(type = CountByCriteriaSqlProvider.class, method = "sql")
    long countByCriteria(E criteria);


    /**
     * 插入provider
     */
    class InsertSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param context context
         * @return  sql
         */
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .INSERT_INTO(table.tableName)
                    .INTO_COLUMNS(table.columns)
                    .INTO_VALUES(Stream.of(table.fields).map(TableInfo::bindParameter).toArray(String[]::new))
                    .toString();

        }
    }

    /**
     * 插入非空字段
     */
    class InsertSelectiveSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param entity  entity
         * @param context context
         * @return  sql
         */
        public String sql(Object entity, ProviderContext context) {
            TableInfo table = tableInfo(context);

            Field[] notNullFields = Stream.of(table.fields)
                    .filter(field -> ReflectionUtils.getFieldValue(field, entity) != null && !table.primaryKeyColumn.equals(TableInfo.columnName(field)))
                    .toArray(Field[]::new);

            return new SQL()
                    .INSERT_INTO(table.tableName)
                    .INTO_COLUMNS(TableInfo.columns(notNullFields))
                    .INTO_VALUES(Stream.of(notNullFields).map(TableInfo::bindParameter).toArray(String[]::new))
                    .toString();

        }
    }

    /**
     * 批量插入provider
     */
    class BatchInsertSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param param  mybatis @Param注解绑定的param map
         * @param context context
         * @return  sql
         */
        public String sql(Map<String, Object> param, ProviderContext context) {
            TableInfo table = tableInfo(context);
            @SuppressWarnings("unchecked")
            int size = ((List<Object>)param.get("entities")).size();
            // 构造 ( #{entities[1-->数组索引].fieldName}, #{entities[1].fieldName2})
            String value = "(" + String.join(",", Stream.of(table.fields)
                    .map(field -> "#{entities[${index}]." + field.getName() + "}").toArray(String[]::new)) + ")";
            String[] values = new String[size];
            Map<String, Object> fillIndex = new HashMap<>(2);
            for (int i = 0; i < size; i++) {
                fillIndex.put("index", i);
                values[i] = PlaceholderResolver.getDefaultResolver().resolveByMap(value, fillIndex);
            }

            SQL sql = new SQL()
                    .INSERT_INTO(table.tableName)
                    .INTO_COLUMNS(table.columns);
            return sql.toString() + " VALUES " + String.join(",", values);
        }
    }

    /**
     * 更新provider
     */
    class UpdateSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param context context
         * @return  sql
         */
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .UPDATE(table.tableName)
                    .SET(Stream.of(table.fields)
                            .filter(field -> !table.primaryKeyColumn.equals(TableInfo.columnName(field)))
                            .map(TableInfo::assignParameter).toArray(String[]::new))
                    .WHERE(table.getPrimaryKeyWhere())
                    .toString();
        }
    }

    /**
     * 只能新非空字段 provider
     */
    class UpdateSelectiveSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param entity  entity
         * @param context context
         * @return  sql
         */
        public String sql(Object entity, ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .UPDATE(table.tableName)
                    .SET(Stream.of(table.fields)
                            .filter(field -> ReflectionUtils.getFieldValue(field, entity) != null && !table.primaryKeyColumn.equals(TableInfo.columnName(field)))
                            .map(TableInfo::assignParameter).toArray(String[]::new))
                    .WHERE(table.getPrimaryKeyWhere())
                    .toString();
        }
    }

    /**
     * 删除provider
     */
    class DeleteSqlProvider extends BaseSqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .DELETE_FROM(table.tableName)
                    .WHERE(table.primaryKeyColumn + " = #{id}")
                    .toString();
        }
    }

    /**
     * 伪删除
     */
    class FakeDeleteSqlProvider extends BaseSqlProviderSupport {
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .UPDATE(table.tableName)
                    .SET("is_deleted = 1")
                    .WHERE(table.primaryKeyColumn + " = #{id}")
                    .toString();
        }
    }

    /**
     * 根据条件删除
     */
    class DeleteByCriteriaSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param criteria  entity condition
         * @param context context
         * @return  sql
         */
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .DELETE_FROM(table.tableName)
                    .WHERE(Stream.of(table.fields)
                            .filter(field -> ReflectionUtils.getFieldValue(field, criteria) != null)
                            .map(TableInfo::assignParameter)
                            .toArray(String[]::new))
                    .toString();
        }
    }

    /**
     * 单条数据查询
     */
    class SelectOneSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param context context
         * @return  sql
         */
        public String sql(ProviderContext context) {
            TableInfo table = tableInfo(context);

            return new SQL()
                    .SELECT(table.selectColumns)
                    .FROM(table.tableName)
                    .WHERE(table.getPrimaryKeyWhere())
                    .toString();
        }
    }

    /**
     * 查询所有记录
     */
    class SelectAllSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param orderBy  排序字段
         * @param context context
         * @return  sql
         */
        public String sql(String orderBy, ProviderContext context) {
            TableInfo table = tableInfo(context);
            SQL sql = new SQL()
                    .SELECT(table.selectColumns)
                    .FROM(table.tableName);
            if (StringUtils.isEmpty(orderBy)) {
                orderBy = table.primaryKeyColumn + " DESC";
            }
            return sql.ORDER_BY(orderBy).toString();
        }
    }

    /**
     * 根据id列表查询
     */
    class SelectByPrimaryKeyInSqlProvider extends BaseSqlProviderSupport {
        public String sql(Map<String, Object> params, ProviderContext context) {
            @SuppressWarnings("unchecked")
            List<Object> ids = (List<Object>)params.get("ids");
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT(table.selectColumns)
                    .FROM(table.tableName)
                    .WHERE(table.primaryKeyColumn
                            + " IN (" + String.join(",", ids.stream().map(String::valueOf).toArray(String[]::new)) +")")
                    .toString();
        }
    }

    /**
     * 根据条件查询
     */
    class SelectByCriteriaSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param criteria  entity 条件
         * @param context context
         * @return  sql
         */
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT(table.selectColumns)
                    .FROM(table.tableName)
                    .WHERE(Stream.of(table.fields)
                            .filter(field -> ReflectionUtils.getFieldValue(field, criteria) != null)
                            .map(TableInfo::assignParameter)
                            .toArray(String[]::new)).ORDER_BY(table.primaryKeyColumn + " DESC").toString();
        }
    }

    /**
     * 根据条件统计
     */
    class CountByCriteriaSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param criteria  entity 条件
         * @param context context
         * @return  sql
         */
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT("COUNT(*)")
                    .FROM(table.tableName)
                    .WHERE(Stream.of(table.fields)
                            .filter(field -> ReflectionUtils.getFieldValue(field, criteria) != null)
                            .map(TableInfo::assignParameter).toArray(String[]::new))
                    .toString();
        }
    }

    /**
     * 统计所有数据
     */
    class CountSqlProvider extends BaseSqlProviderSupport {
        /**
         * sql
         * @param criteria  entity 条件
         * @param context context
         * @return  sql
         */
        public String sql(Object criteria, ProviderContext context) {
            TableInfo table = tableInfo(context);
            return new SQL()
                    .SELECT("COUNT(*)")
                    .FROM(table.tableName)
                    .toString();
        }
    }


    /**
     * 基类
     */
    abstract class BaseSqlProviderSupport {
        /**
         * key -> mapper class   value -> tableInfo
         */
        private static Map<Class<?>, TableInfo> tableCache = new ConcurrentHashMap<>(128);

        /**
         * 获取表信息结构
         *
         * @param context  provider context
         * @return  表基本信息
         */
        protected TableInfo tableInfo(ProviderContext context) {
            // 如果不存在则创建
            return tableCache.computeIfAbsent(context.getMapperType(), TableInfo::of);
        }
    }



    /**
     * table info
     *
     * @author meilin.huang
     * @date 2020-02-16 3:50 下午
     */
    class TableInfo {
        /**
         * 表前缀
         */
        private static final String TABLE_PREFIX = "tb_";

        /**
         * 主键名
         */
        private static final String DEFAULT_PRIMARY_KEY = "id";

        /**
         * 表名
         */
        private String tableName;

        /**
         * 实体类型不含@NoColunm注解的field
         */
        private Field[] fields;

        /**
         * 主键列名
         */
        private String primaryKeyColumn;

        /**
         * 所有列名
         */
        private String[] columns;

        /**
         * 所有select sql的列名,有带下划线的将其转为aa_bb AS aaBb
         */
        private String[] selectColumns;

        private TableInfo() {}

        /**
         * 获取主键的where条件,如 id = #{id}
         *
         * @return  主键where条件
         */
        public String getPrimaryKeyWhere() {
            String pk = this.primaryKeyColumn;
            return pk + " = #{" + pk + "}";
        }

        /**
         * 获取TableInfo的简单工厂
         *
         * @param mapperType mapper类型
         * @return            {@link TableInfo}
         */
        public static TableInfo of(Class<?> mapperType) {
            Class<?> entityClass = entityType(mapperType);
            // 获取不含有@NoColumn注解的fields
            Field[] fields = excludeNoColumnField(entityClass);
            TableInfo tableInfo = new TableInfo();
            tableInfo.fields = fields;
            tableInfo.tableName = tableName(entityClass);
            tableInfo.primaryKeyColumn =  primaryKeyColumn(fields);
            tableInfo.columns = columns(fields);
            tableInfo.selectColumns = selectColumns(fields);
            return tableInfo;
        }

        /**
         * 获取BaseMapper接口中的泛型类型
         *
         * @param mapperType  mapper类型
         * @return       实体类型
         */
        public static Class<?> entityType(Class<?> mapperType) {
            return Stream.of(mapperType.getGenericInterfaces())
                    .filter(ParameterizedType.class::isInstance)
                    .map(ParameterizedType.class::cast)
                    .filter(type -> type.getRawType() == BaseMapper.class)
                    .findFirst()
                    .map(type -> type.getActualTypeArguments()[1])
                    .filter(Class.class::isInstance).map(Class.class::cast)
                    .orElseThrow(() -> new IllegalStateException("未找到BaseMapper的泛型类 " + mapperType.getName() + "."));
        }


        /**
         * 获取表名
         *
         * @param entityType  实体类型
         * @return      表名
         */
        public static String tableName(Class<?> entityType) {
            Table table = entityType.getAnnotation(Table.class);
            return table == null ? TABLE_PREFIX + StringUtils.camel2Underscore(entityType.getSimpleName()) : table.value();
        }

        /**
         * 过滤含有@NoColumn注解或者是静态的field
         *
         * @param entityClass 实体类型
         * @return 不包含@NoColumn注解的fields
         */
        public static Field[] excludeNoColumnField(Class<?> entityClass) {
            Field[] allFields = ReflectionUtils.getFields(entityClass);
            List<String> excludeColumns = getClassExcludeColumns(entityClass);
            return Stream.of(allFields)
                    //过滤掉类上指定的@NoCloumn注解的字段和字段上@NoColumn注解或者是静态的field
                    .filter(field -> !CollectionUtils.contains(excludeColumns, field.getName())
                            && (!field.isAnnotationPresent(NoColumn.class) && !Modifier.isStatic(field.getModifiers())))
                    .toArray(Field[]::new);
        }

        /**
         * 获取实体类上标注的不需要映射的字段名
         *
         * @param entityClass  实体类
         * @return             不需要映射的字段名
         */
        public static List<String> getClassExcludeColumns(Class<?> entityClass) {
            List<String> excludeColumns = null;
            NoColumn classNoColumns = entityClass.getAnnotation(NoColumn.class);
            if (classNoColumns != null) {
                excludeColumns = Arrays.asList(classNoColumns.fields());
            }
            return excludeColumns;
        }

        /**
         * 获取查询对应的字段 (不包含pojo中含有@NoColumn主键的属性)
         *
         * @param fields p
         * @return  所有需要查询的查询字段
         */
        public static String[] selectColumns(Field[] fields) {
            return Stream.of(fields).map(TableInfo::selectColumnName).toArray(String[]::new);
        }

        /**
         * 获取所有pojo所有属性对应的数据库字段 (不包含pojo中含有@NoColumn主键的属性)
         *
         * @param fields entityClass所有fields
         * @return        所有的column名称
         */
        public static String[] columns(Field[] fields) {
            return Stream.of(fields).map(TableInfo::columnName).toArray(String[]::new);
        }

        /**
         * 如果fields中含有@Primary的字段,则返回该字段名为主键,否则默认'id'为主键名
         *
         * @param fields entityClass所有fields
         * @return 主键column(驼峰转为下划线)
         */
        public static String primaryKeyColumn(Field[] fields) {
            return Stream.of(fields).filter(field -> field.isAnnotationPresent(PrimaryKey.class))
                    .findFirst()    //返回第一个primaryKey的field
                    .map(TableInfo::columnName)
                    .orElse(DEFAULT_PRIMARY_KEY);
        }

        /**
         * 获取单个属性对应的数据库字段(带有下划线字段将其转换为"字段 AS pojo属性名"形式)
         *
         * @param field  字段
         * @return      带有下划线字段将其转换为"字段 AS pojo属性名"形式
         */
        public static String selectColumnName(Field field) {
            String camel = StringUtils.camel2Underscore(field.getName());
            return camel.contains("_") ? camel + " AS " + field.getName() : camel;
        }

        /**
         * 获取单个属性对应的数据库字段
         *
         * @param field entityClass中的field
         * @return  字段对应的column
         */
        public static String columnName(Field field) {
            return StringUtils.camel2Underscore(field.getName());
        }

        /**
         * 绑定参数
         *
         * @param field  字段
         * @return        参数格式
         */
        public static String bindParameter(Field field) {
            return "#{" + field.getName() + "}";
        }

        /**
         * 获取该字段的参数赋值语句,如 user_name = #{userName}
         * @param field  字段
         * @return       参数赋值语句
         */
        public static String assignParameter(Field field) {
            return columnName(field) + " = " + bindParameter(field);
        }
    }
}

以上就是BaseMapper的主要代码,就可以轻松实现一些通用的mapper方法。以上代码中还有些其他对象(如TableInfo以及@NoColume注解等),由于文章篇幅已经太多了。如果有需要的可以在个人的项目中查看,并copy。
个人项目之BaseMapper目录链接:https://gitee.com/objs/mayfly/tree/master/mayfly-dao/src/main/java/mayfly/dao/base

使用方法

/**
*声明一个接口,继承BaseMapper接口,并将实体类传入BaseMapper的泛型参数中
*/
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {

}

也可以使用@NoColume注解过滤非数据库表中的字段,加了该注解之后通用查询时候就不会查该属性,主要用户复合对象如:

public class User{
    @NoColume
    private Product product;
}

接下来就可以在service中注入对应Mapper就可以实现通用方法调用了。

总结

有了通用Mapper可以大幅减轻重复的工作量。个人项目中也有一些通用Service等通用功能,如感兴趣可前往查看,并使用,当然可能也存在BUG,欢迎大佬多多指导!
BaseService代码链接:https://gitee.com/objs/mayfly/tree/master/mayfly-sys/src/main/java/mayfly/sys/service/base

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

推荐阅读更多精彩内容