从API获取JSON格式数据存储到数据库中

基础依赖

mybatisplus、springboot

需求

如果需要从API获取数据并存入数据库中,可以尝试以下方法。

必要条件

首先需要创建对应实体类、并且实现KeyInterface
其次实现它的对应Service

tableEntity实体类

@Data
@TableName("tableName")
public class tableEntity implements KeyInterface {
    @Override
    public String getKey() {
        return getPrefix()+getUserId();
    }

    public String getPrefix() {
        String tableName = this.getClass().getAnnotation(TableName.class).value();
        String camel = ToCamelUtils.toCamel(tableName);
        return "Sync"+camel+":";
    }

    /**
     * 用户id
     */
    @TableField(value = "user_id")
    @SerializedName("Userid")
    private String userId;

    /**
     * 名称
     */
    @TableField(value = "user_name")
    @SerializedName("UserName")
    private String userName;
}

keyinterface

public interface KeyInterface {
    String getKey();

    String getPrefix();
}

这里需要将每个实体类对应的数据库字段名使用@TableField注解将对应JSON数据取出,因此在获取到JSON数据后,建表的字段需要与JSON字段对应,避免在处理时因为找不到对应字段造成的数据丢失。
在实现了对应的实体类后还需要实现对应实体类的Service

Service

@Service
public class tableEntityService extends ServiceImpl<tableEntityDao, tableEntity> implements BatchInterface {

    @Override
    public boolean batchInsert(List list) {
        return baseMapper.batchInsert(list);
    }

    @Override
    public boolean batchUpdate(List list) {
        return baseMapper.batchUpdate(list);
    }
}

BatchInterface

public interface BatchInterface<T extends BaseETLEntity> {
    public boolean batchInsert(List<T> list);
    public boolean batchUpdate(List<T> list);
}

service需要实现BatchInterface方法,用于实现批量保存和批量更新方法,在写这个功能时需要根据json数据将库中数据进行更新或插入因此需要使用实现这两个方法以提高效率。
在实现对应Service和Entity后就可以开始着手实现通用的处理方法了

SyncService

@Slf4j
public class ETLSyncService<ServiceImpl extends com.baomidou.mybatisplus.extension.service.impl.ServiceImpl & BatchInterface, Target extends SyncBaseETLEntity & KeyInterface,Mapper extends BaseMapper<Target> >{
    private String tableName;
//    private List<String> keys;
    @Autowired
    private ServiceImpl targetService;
    @Autowired
    private RedisUtils redisUtils;
    private static final String INSERT = "INSERT";
    private static final String UPDATE = "UPDATE";
    private static final boolean PASS = true;
    private static final Long DEFAULT_EXPIRED= Long.valueOf(60 * 30) ;
    @Transactional(rollbackFor = Exception.class)
    public ResultModel executeSync(String tableName ,JSONArray jsonArray) throws IllegalAccessException, InstantiationException {
        beforeSync();
        Service serviceAnnotion = this.getClass().getAnnotation(Service.class);
        if (serviceAnnotion == null) {
            log.error("对接接口错误:"+ this.getClass().getName()+"@Service注解为空,无法获取表名");
            throw new GlobalException("对接接口错误:"+ this.getClass().getName()+" @Service注解为空,无法获取表名");
        }
        Set<Target> updateList = new HashSet<>();
        Set<Target> saveList = new HashSet<>();
        LocalDateTime now = LocalDateTime.now();
        cleanRedis(tableName);
        boolean isEmpty = getAndSaveInRedis();
        this.tableName = tableName;
        try {
            if (!isEmpty) {
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    Target target = convert(jsonObject,now);
                    // 如果target返回null就下一个
                    if (target == null) {
                        continue;
                    }
                    Target origin = (Target) redisUtils.get(target.getKey(),target.getClass());
                    if (origin == null) {
                        saveList.add(target);
                        continue;
                    }
                    if (!isSame(target,origin,saveList,updateList,now)) {
                        target.setRegDate(origin.getRegDate());
                        target.setRegPsn(origin.getRegPsn());
                        updateList.add(target);
                    }
                }
                if (!saveList.isEmpty()) {
                    this.saveOrUpdateList(new ArrayList<>(saveList),INSERT);
                }
                if (!updateList.isEmpty()) {
                    this.saveOrUpdateList(new ArrayList<>(updateList),UPDATE);
                }
            } else {
                // 需要同步的表为空则直接把获取到的数据全部插入
                for (int j = 0 ; j < jsonArray.size();j++) {
                    Target target = convert(jsonArray.getJSONObject(j),now);
                    if (target == null) {
                        continue;
                    }
                    saveList.add(target);
                }
                saveOrUpdateList(new ArrayList<>(saveList),INSERT);
            }
        }
        catch (IllegalAccessException illegalAccessException) {
            log.error("ETL对接接口错误:"+ this.getClass().getName()+"异常信息:"+illegalAccessException);
            return ResultModel.error(illegalAccessException.getMessage());
        }
        catch (InstantiationException instantiationException) {
            log.error("ETL对接接口错误:"+ this.getClass().getName()+"异常信息:"+instantiationException);
            return ResultModel.error(instantiationException.getMessage());
        }
        catch (GlobalException globalException) {
            globalException.printStackTrace();
            log.error("ETL对接接口错误:"+globalException.getMsg());
            return ResultModel.error(globalException.getMsg());
        }
        catch (Exception exception) {
            exception.printStackTrace();
            log.error("ETL对接接口错误:"+exception);
            return ResultModel.error(exception.getMessage());
        }
        return ResultModel.success();
    }
    public void beforeSync() {}
    // 批量删除相关前缀的key
    public boolean cleanRedis(String camelTableName) {
        String prefix = "Sync"+camelTableName+":*";
        Set<String> keysList = redisUtils.getKeys(prefix);
        redisUtils.batchDelete(new ArrayList<>(keysList));
        return true;
    }
    public boolean getAndSaveInRedis() {
        List<Target> allList = targetService.list();
        if (allList.isEmpty()) {
            return true;
        }
//        for (Target target : allList) {
//            if (StringUtils.isBlank(target.getKey())) {
//                throw new GlobalException("ETL对接接口错误: 主键为空");
//            }
//            redisUtils.set(target.getKey(),target,3600);
//        }
        redisUtils.batchSet(allList.stream().collect(Collectors.toConcurrentMap(target->target.getKey(),target->target)),DEFAULT_EXPIRED);
        return false;
    }
    private void saveOrUpdateList(List<Target> list,String operationType) {
        if(!checkPoint(list)) {
            log.error("检查点不通过");
            return;
        }
        switch (operationType) {
            case INSERT:
                this.batchInsert(list);
                break;
            case UPDATE:
                this.batchUpdate(list);
                break;
            default:
                log.error("操作类型错误:"+operationType);
                break;
        }
    }
    // 可以重写该方法来检验检查点
    public boolean checkPoint(List<Target> target) {
        return PASS;
    }
    public boolean isPropertiesLegel(JSONObject jsonObject) {return true;}
    // 将JSONObject转换成目标类型
    public Target convert(JSONObject jsonObject,LocalDateTime now) throws IllegalAccessException, InstantiationException {
        if (jsonObject == null) {
            throw new GlobalException("ETLSync同步失败: 数据为空");
        }
        // 可以重写该方法来校验JSONObject的数据是否合格,返回空的数据就不插入表内
        if (!isPropertiesLegel(jsonObject)) {
            return null;
        }
        Class<?> targetClass = GenericsUtils.getSuperClassGenricType(this.getClass(),1);
        Target target = (Target) targetClass.newInstance();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            TableField tableField = field.getAnnotation(TableField.class);
            if (tableField == null) {
                log.error("ETL对接接口错误:"+ field.getName()+"@TableField注解为空,无法获取字段名");
                throw new GlobalException("ETL对接接口错误:"+ field.getName()+" @TableField注解为空,无法获取字段名");
            }
            if (tableField.exist() == false) {
                continue;
            }
            // 根据数据库中的字段名与Json的key做匹配
            String tableName = tableField.value();
            field.setAccessible(true);
            // 先直接根据注解的获取的字段名与json的key对比
            Object obj = jsonObject.get(tableName);
            if (obj == null) {
                // 找不到就根据驼峰命名去找
                String toCamel = ToCamelUtils.toCamel(tableName);
                for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                    if(entry.getKey().equalsIgnoreCase(toCamel)) {
                        obj = entry.getValue();

                        break;
                    }
                }
            }
            // 还找不到就忽略大小写去匹配字段名与json的key
            if (obj == null) {
                for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                    if(entry.getKey().equalsIgnoreCase(tableName)) {
                        obj = entry.getValue();
                        break;
                    }
                }
            }
            // 判断是否能转换成日期
            try {
                // 判断是否为字符串
                if (obj instanceof String) {
                    LocalDateTime localDateTime = LocalDateTime.parse((String)obj);
                    field.set(target,localDateTime);
                } else {
                    field.set(target,obj);
                }
            } catch (DateTimeParseException e) {
                field.set(target,obj);
            }
        }
        target.setUpdDate(now);
        target.setRegDate(now);
        target.setUpdPsn("Sync"+tableName);
        target.setRegPsn("Sync"+tableName);
        return target;
    }
    // 判断两者是否相同
    public boolean isSame(Target target,Target origin,Set<Target> saveList,Set<Target> updateTarget,LocalDateTime now) throws IllegalAccessException, InstantiationException {
        Field[] fields = GenericsUtils.getSuperClassGenricType(this.getClass(),1).getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if ("regPsn".equalsIgnoreCase(field.getName()) || "regDate".equalsIgnoreCase(field.getName()) ||
            "updPsn".equalsIgnoreCase(field.getName()) || "updDate".equalsIgnoreCase(field.getName())) {
                continue;
            }
            if (field.get(target) == null && field.get(origin) != null) {
                return false;
            } else if (field.get(target) != null && field.get(origin) == null) {
                return false;
            } else if (field.get(target) == null && field.get(origin) == null){
                continue;
            }
            if (!field.get(target).equals(field.get(origin))) {
                return false;
            }
        }
        for (Target temp : saveList) {
            if (temp.equals(target)) {
                return true;
            }
        }
        for (Target temp : updateTarget) {
            if (temp.equals(target)) {
                return true;
            }
        }
        return true;
    }
    // 批量保存方法,如果字段超过21个则需要重写该方法
    public boolean batchInsert(List<Target> targetList) {
        Consumer<List<Target>> saveConsumer = (batch) -> {
            targetService.batchInsert(batch);
        };
        SqlserverBatchOperationUtil.processInBatches(targetList,100,saveConsumer);
        return true;
    }
    // 批量更新方法,如果字段超过21个则需要重写该方法
    public boolean batchUpdate(List<Target> targetList) {
        Consumer<List<Target>> updateConsumer = (batch) -> {
            targetService.batchUpdate(batch);
        };
        SqlserverBatchOperationUtil.processInBatches(targetList,100,updateConsumer);
        return true;
    }
    // 如果需要在全量同步之前先清空数据要重写该方法
    public boolean deleteAll () {
        return true;
    }
}

接下来将分析这个类中实现的方法

 // 批量保存方法,如果字段超过21个则需要重写该方法
    public boolean batchInsert(List<Target> targetList) {
        Consumer<List<Target>> saveConsumer = (batch) -> {
            targetService.batchInsert(batch);
        };
        SqlserverBatchOperationUtil.processInBatches(targetList,100,saveConsumer);
        return true;
    }
    // 批量更新方法,如果字段超过21个则需要重写该方法
    public boolean batchUpdate(List<Target> targetList) {
        Consumer<List<Target>> updateConsumer = (batch) -> {
            targetService.batchUpdate(batch);
        };
        SqlserverBatchOperationUtil.processInBatches(targetList,100,updateConsumer);
        return true;
    }
    // 如果需要在全量同步之前先清空数据要重写该方法
    public boolean deleteAll () {
        return true;
    }

上列三个方法分别用于批量保存、批量更新、以及全量删除
如果有其他需求,例如在保存api中的json数据时需要先全量删除时就在继承SyncService的类中重写这个deleteAll方法,并且如同注释所示
sqlserver在执行sql语句时超过2100个参数则会报错,因此在单条json数据超过21个字段时就必须要重写batchUpdate方法和batchInsert方法避免报错

    // 可以重写该方法来检验检查点
    public boolean checkPoint(List<Target> target) {
        return PASS;
    }
    public boolean isPropertiesLegel(JSONObject jsonObject) {return true;}
    // 将JSONObject转换成目标类型
    public Target convert(JSONObject jsonObject,LocalDateTime now) throws IllegalAccessException, InstantiationException {
        if (jsonObject == null) {
            throw new GlobalException("ETLSync同步失败: 数据为空");
        }
        // 可以重写该方法来校验JSONObject的数据是否合格,返回空的数据就不插入表内
        if (!isPropertiesLegel(jsonObject)) {
            return null;
        }
        Class<?> targetClass = GenericsUtils.getSuperClassGenricType(this.getClass(),1);
        Target target = (Target) targetClass.newInstance();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            TableField tableField = field.getAnnotation(TableField.class);
            if (tableField == null) {
                log.error("对接接口错误:"+ field.getName()+"@TableField注解为空,无法获取字段名");
                throw new GlobalException("对接接口错误:"+ field.getName()+" @TableField注解为空,无法获取字段名");
            }
            if (tableField.exist() == false) {
                continue;
            }
            // 根据数据库中的字段名与Json的key做匹配
            String tableName = tableField.value();
            field.setAccessible(true);
            // 先直接根据注解的获取的字段名与json的key对比
            Object obj = jsonObject.get(tableName);
            if (obj == null) {
                // 找不到就根据驼峰命名去找
                String toCamel = ToCamelUtils.toCamel(tableName);
                for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                    if(entry.getKey().equalsIgnoreCase(toCamel)) {
                        obj = entry.getValue();

                        break;
                    }
                }
            }
            // 还找不到就忽略大小写去匹配字段名与json的key
            if (obj == null) {
                for (Map.Entry<String, Object> entry : jsonObject.entrySet()) {
                    if(entry.getKey().equalsIgnoreCase(tableName)) {
                        obj = entry.getValue();
                        break;
                    }
                }
            }
            // 判断是否能转换成日期
            try {
                // 判断是否为字符串
                if (obj instanceof String) {
                    LocalDateTime localDateTime = LocalDateTime.parse((String)obj);
                    field.set(target,localDateTime);
                } else {
                    field.set(target,obj);
                }
            } catch (DateTimeParseException e) {
                field.set(target,obj);
            }
        }
        return target;
    }

convert方法用于把json中的数据转换成对应的实体类,再存入list中保存至数据库中
isPropertiesLegel用于检验对应的json是否符合某些规则,可以在继承了SyncService的类中重写该方法来达成校验的目的,这样就能提前排除某些不需要的数据
checkPoint也是在执行更新或插入之前通过重写该方法来决定是否需要将数据更新或插入到数据库中

public ResultModel executeSync(String tableName ,JSONArray jsonArray) throws IllegalAccessException, InstantiationException {
        beforeSync();
        Service serviceAnnotion = this.getClass().getAnnotation(Service.class);
        if (serviceAnnotion == null) {
            log.error("对接接口错误:"+ this.getClass().getName()+"@Service注解为空,无法获取表名");
            throw new GlobalException("对接接口错误:"+ this.getClass().getName()+" @Service注解为空,无法获取表名");
        }
        Set<Target> updateList = new HashSet<>();
        Set<Target> saveList = new HashSet<>();
        LocalDateTime now = LocalDateTime.now();
        cleanRedis(tableName);
        boolean isEmpty = getAndSaveInRedis();
        this.tableName = tableName;
        try {
            if (!isEmpty) {
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    Target target = convert(jsonObject,now);
                    // 如果target返回null就下一个
                    if (target == null) {
                        continue;
                    }
                    Target origin = (Target) redisUtils.get(target.getKey(),target.getClass());
                    if (origin == null) {
                        saveList.add(target);
                        continue;
                    }
                    if (!isSame(target,origin,saveList,updateList,now)) {
                        target.setRegDate(origin.getRegDate());
                        target.setRegPsn(origin.getRegPsn());
                        updateList.add(target);
                    }
                }
                if (!saveList.isEmpty()) {
                    this.saveOrUpdateList(new ArrayList<>(saveList),INSERT);
                }
                if (!updateList.isEmpty()) {
                    this.saveOrUpdateList(new ArrayList<>(updateList),UPDATE);
                }
            } else {
                // 需要同步的表为空则直接把获取到的数据全部插入
                for (int j = 0 ; j < jsonArray.size();j++) {
                    Target target = convert(jsonArray.getJSONObject(j),now);
                    if (target == null) {
                        continue;
                    }
                    saveList.add(target);
                }
                saveOrUpdateList(new ArrayList<>(saveList),INSERT);
            }
        }
        catch (IllegalAccessException illegalAccessException) {
            log.error("ETL对接接口错误:"+ this.getClass().getName()+"异常信息:"+illegalAccessException);
            return ResultModel.error(illegalAccessException.getMessage());
        }
        catch (InstantiationException instantiationException) {
            log.error("ETL对接接口错误:"+ this.getClass().getName()+"异常信息:"+instantiationException);
            return ResultModel.error(instantiationException.getMessage());
        }
        catch (GlobalException globalException) {
            globalException.printStackTrace();
            log.error("ETL对接接口错误:"+globalException.getMsg());
            return ResultModel.error(globalException.getMsg());
        }
        catch (Exception exception) {
            exception.printStackTrace();
            log.error("ETL对接接口错误:"+exception);
            return ResultModel.error(exception.getMessage());
        }
        return ResultModel.success();
    }
    public void beforeSync() {}
    // 批量删除相关前缀的key
    public boolean cleanRedis(String camelTableName) {
        String prefix = "Sync"+camelTableName+":*";
        Set<String> keysList = redisUtils.getKeys(prefix);
        redisUtils.batchDelete(new ArrayList<>(keysList));
        return true;
    }
    public boolean getAndSaveInRedis() {
        List<Target> allList = targetService.list();
        if (allList.isEmpty()) {
            return true;
        }
        redisUtils.batchSet(allList.stream().collect(Collectors.toConcurrentMap(target->target.getKey(),target->target)),DEFAULT_EXPIRED);
        return false;
    }

cleanRedis 在同步之前需要先清空Redis的缓存,避免因为缓存导致数据没有及时更新
getAndSaveInRedis 在更新前将数据库中的数据存入缓存避免持续对数据库进行访问
beforeSync 通过重写该方法,实现执行同步之前的方法

executeSync

这个方法是整个同步方法的执行方法,首先通过Service注解获取到具体实现类,在保存至redis的方法中判断数据库是否为空,如果为空则跳过更新判断直接把所有数据存入数据库中,不为空则判断是否在数据库中已存在并且是否与json数据不一致,如果不一致则更新。
整个模板方法到此就完成了
接下来只需要写一个Service来继承这个SyncService即可同步任意字段的json

@Service(value = "SyncPmsUserInfoService")
public class SyncUserInfoService extends SyncService<UserInfoService, UserInfoEntity,UserInfoDao>{
    @Autowired
    private PmsUserInfoService pmsUserInfoService;
    @Override
    public boolean deleteAll() {
        QueryWrapper wrapper = new QueryWrapper();
        return pmsUserInfoService.remove(wrapper);
    }
}

接口

private ResultModel syncToDB(Map<String, Object> params, JobLogEntity jobLogEntity) {
        ResultModel resultModel = new ResultModel();
        try {
            String tableName = MapUtil.getStr(params,"tableName");
            String isDeleteAll = MapUtil.getStr(params,"isDeleteAll");
            if (StringUtils.isBlank(tableName)) {
                return ResultModel.error("同步至ETL失败:tableName不能为空");
            }
            tableName = ToCamelUtils.toCamel(tableName);
            String serviceName = "Sync"+tableName+"Service";
            Object service = SpringContextUtils.getBean(serviceName);
            if (service instanceof SyncService) {
                ETLSyncService syncService = (ETLSyncService) service;
                if (StringUtils.isNotBlank(isDeleteAll)) {
                    if ("true".equalsIgnoreCase(isDeleteAll)) {
                        syncService.deleteAll();
                    }
                }
                JSONArray jsonArray = syncApiUtil.getApiAsJSONArray(tableName);
                if (jsonArray == null) {
                    throw new GlobalException("同步失败:Api数据获取失败");
                }
                resultModel = syncService.executeSync(tableName,jsonArray);
            }
            if (resultModel.isSuccess()) {
                updateJob(bizPrcJobLogEntity, "success", "");
            } else {
                updateJob(jobLogEntity, "error", (String)resultModel.get("msg"));
            }
            return resultModel;
        }
        catch (GlobalException e) {
            e.printStackTrace();
            resultModel.put("code", -1);
            resultModel.put("msg", e.getMsg());
            return resultModel;
        }
        catch (BeansException e) {
            e.printStackTrace();
            resultModel.put("code", -1);
            resultModel.put("msg", "获取对应Service失败");
            return resultModel;
        }
        catch (Exception e) {
            e.printStackTrace();
            resultModel.put("code", -1);
            return resultModel;
        }
    }

可以写一个这样的接口,根据传入的tableName参数获取具体需要同步数据的表,这样就不需要为每一个同步数据的功能去创建功能类似的接口了

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

推荐阅读更多精彩内容