Apollo 10 — adminService 全量發(fā)布

目錄

  1. UI 界面
  2. Portal 服務(wù)
  3. admin 服務(wù)
  4. 總結(jié)

1. UI 界面

1
2
3

2. Portal 服務(wù)

當(dāng)我們點(diǎn)擊上面的發(fā)布按鈕的時(shí)候萝喘,調(diào)用的當(dāng)然是 portal 的接口。具體代碼如下:

  /**
   * 全量發(fā)布
   * @param appId SampleApp
   * @param env DEV
   * @param clusterName default
   * @param namespaceName  application
   * @param branchName 分支/灰度名稱
   * @param deleteBranch true
   * @param model {"releaseTitle":"20180716220550-gray-release-merge-to-master","releaseComment":"","isEmergencyPublish":false}
   * @return
   */
  @PreAuthorize(value = "@permissionValidator.hasReleaseNamespacePermission(#appId, #namespaceName)")
  @RequestMapping(value = "/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}/branches/{branchName}/merge", method = RequestMethod.POST)
  public ReleaseDTO merge(@PathVariable String appId, @PathVariable String env,
                          @PathVariable String clusterName, @PathVariable String namespaceName,
                          @PathVariable String branchName, @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,
                          @RequestBody NamespaceReleaseModel model) {
    // 如果是緊急發(fā)布,但該環(huán)境不允許緊急發(fā)布,拋出異常
    if (model.isEmergencyPublish() && !portalConfig.isEmergencyPublishAllowed(Env.fromString(env))) {
      throw new BadRequestException(String.format("Env: %s is not supported emergency publish now", env));
    }
    // 合并主版本和灰度版本, 得到一個(gè)發(fā)布 dto
    ReleaseDTO createdRelease = namespaceBranchService.merge(appId, Env.valueOf(env), clusterName, namespaceName, branchName,
                                                             model.getReleaseTitle(), model.getReleaseComment(),
                                                             model.isEmergencyPublish(), deleteBranch);

    ConfigPublishEvent event = ConfigPublishEvent.instance();
    event.withAppId(appId)
        .withCluster(clusterName)
        .withNamespace(namespaceName)
        .withReleaseId(createdRelease.getId())
        .setMergeEvent(true)
        .setEnv(Env.valueOf(env));

    publisher.publishEvent(event);// 發(fā)送郵件

    return createdRelease;
  }

接口職責(zé)不多:是否符合緊急發(fā)布的數(shù)據(jù)校驗(yàn)半沽,調(diào)用 Service, 發(fā)布“配置發(fā)布”事件(發(fā)送郵件)。

看看調(diào)用 Service 的過程或辖,該方法稱為 merge ,實(shí)際上就是合并灰度和主版本的配置枣接。代碼如下:

  public ReleaseDTO merge(String appId, Env env, String clusterName, String namespaceName,
                          String branchName, String title, String comment,
                          boolean isEmergencyPublish, boolean deleteBranch) {
    // 計(jì)算 changeSets
    ItemChangeSets changeSets = calculateBranchChangeSet(appId, env, clusterName, namespaceName, branchName);
    // 調(diào)用 admin 服務(wù)
    ReleaseDTO mergedResult =
        releaseService.updateAndPublish(appId, env, clusterName, namespaceName, title, comment,
                                        branchName, isEmergencyPublish, deleteBranch, changeSets);

    Tracer.logEvent(TracerEventType.MERGE_GRAY_RELEASE,
                 String.format("%s+%s+%s+%s", appId, env, clusterName, namespaceName));

    return mergedResult;
  }

做了 2 件事情: 計(jì)算 change 集合颂暇,調(diào)用 admin 服務(wù)。很明顯但惶,計(jì)算 change 對(duì)于 protal 非常重要耳鸯。

calculateBranchChangeSet 方法主要將灰度配置和主版本配置合并。

代碼:

private ItemChangeSets calculateBranchChangeSet(String appId, Env env, String clusterName, String namespaceName,
                                                String branchName) {
  NamespaceBO parentNamespace = namespaceService.loadNamespaceBO(appId, env, clusterName, namespaceName);// 父版本 namespace

  if (parentNamespace == null) {
    throw new BadRequestException("base namespace not existed");
  }

  if (parentNamespace.getItemModifiedCnt() > 0) {
    throw new BadRequestException("Merge operation failed. Because master has modified items");
  }

  List<ItemDTO> masterItems = itemService.findItems(appId, env, clusterName, namespaceName);// 主版本 items 

  List<ItemDTO> branchItems = itemService.findItems(appId, env, branchName, namespaceName);// 子版本 items 

  ItemChangeSets changeSets = itemsComparator.compareIgnoreBlankAndCommentItem(parentNamespace.getBaseInfo().getId(),
                                                                               masterItems, branchItems);// 得到 changeSet
  changeSets.setDeleteItems(Collections.emptyList());// 防止誤刪除膀曾,emm县爬,灰度的內(nèi)容并不是全量的,因此上面的計(jì)算有些問題添谊,并且目前沒有刪除功能财喳。所以這里可以置空。
  changeSets.setDataChangeLastModifiedBy(userInfoHolder.getUser().getUserId());
  return changeSets;
}

步驟:

  1. 獲取主版本的 namespace 詳細(xì)信息斩狱,用于數(shù)據(jù)檢驗(yàn)耳高,id 賦值。
  2. 獲取主版本的所有 item 配置所踊,再獲取灰度版本的所有 item 配置泌枪,注意,灰度版本的 item 只有其自身新增的和修改的配置污筷,不是全量的(這將導(dǎo)致后面一個(gè)奇怪的現(xiàn)象)工闺。
  3. 比較兩者差異乍赫,得到 change 集合瓣蛀。
  4. 設(shè)置 deleteList 為空 —— 奇怪現(xiàn)象(灰度的內(nèi)容并不是全量的,因此上面的計(jì)算有些問題雷厂,并且目前沒有刪除功能惋增。所以這里可以置空, 并且防止誤刪除)。
  5. 設(shè)置修改人改鲫。

這里需要注意的是計(jì)算差異到底是怎么計(jì)算的诈皿,為什么后面有置空 deleteItem 的操作。

我就不貼全部的方法了像棘,貼一下對(duì)刪除操作有影響的代碼:

/** 比較,忽略空格,返回一個(gè)改變的 items */
public ItemChangeSets compareIgnoreBlankAndCommentItem(long baseNamespaceId, List<ItemDTO> baseItems, List<ItemDTO> targetItems){

  // 忽略新增/修改 item 代碼......

  // 處理刪除,但這個(gè)邏輯似乎不對(duì). 不過此類不知道數(shù)據(jù)來源,工具類沒有問題.
  for (ItemDTO item: baseItems){// 主版本
    String key = item.getKey();

    ItemDTO targetItem = targetItemMap.get(key);
    if(targetItem == null){//delete// 如果灰度版本里沒有,說明刪除了.
      changeSets.addDeleteItem(item);// 添加進(jìn)刪除集合
    }
  }
  return changeSets;
}

可以看到稽亏,這段代碼里,循環(huán)主版本缕题,逐個(gè)對(duì)比灰度版本截歉,如果灰度版本里沒有,就添加進(jìn) delete 集合烟零,而我們知道瘪松,灰度版本的 item 只有修改的和新增的咸作,這時(shí),將導(dǎo)致誤刪除宵睦。

但這個(gè)工具類的計(jì)算是沒有問題的记罚,有問題的是外層數(shù)據(jù)的完整性。

因此需要在外面打個(gè)補(bǔ)犊呛俊:changeSets.setDeleteItems(Collections.emptyList());

好桐智,計(jì)算完 changeSet,就要調(diào)用 admin 服務(wù)了烟馅,并且把 changeSet 傳遞過去酵使,然后返回一個(gè) release 對(duì)象,表示發(fā)布成功焙糟,并發(fā)布事件口渔。

在分析 admin 之前,總結(jié)一下 protal 的流程:

3. admin 服務(wù)

從 portal 的代碼中穿撮,可以看到缺脉,調(diào)用的是 admin 的 updateAndPublish 方法接口,看看這個(gè)接口:
位置 : com.ctrip.framework.apollo.adminservice.controller.ReleaseController.java
代碼如下:

  @Transactional
  @RequestMapping(path = "/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/updateAndPublish", method = RequestMethod.POST)
  public ReleaseDTO updateAndPublish(@PathVariable("appId") String appId,// 應(yīng)用名稱
                                     @PathVariable("clusterName") String clusterName,//集群
                                     @PathVariable("namespaceName") String namespaceName,// 主版本名稱
                                     @RequestParam("releaseName") String releaseName, // 發(fā)布名稱
                                     @RequestParam("branchName") String branchName,// 灰度名稱 cluster
                                     @RequestParam(value = "deleteBranch", defaultValue = "true") boolean deleteBranch,// 是否刪除灰度
                                     @RequestParam(name = "releaseComment", required = false) String releaseComment,// 評(píng)論
                                     @RequestParam(name = "isEmergencyPublish", defaultValue = "false") boolean isEmergencyPublish,// 是否緊急發(fā)布
                                     @RequestBody ItemChangeSets changeSets) {// 這個(gè)是 portal 發(fā)來的
    Namespace namespace = namespaceService.findOne(appId, clusterName, namespaceName);// 找到分支
    if (namespace == null) {
      throw new NotFoundException(String.format("Could not find namespace for %s %s %s", appId,
                                                clusterName, namespaceName));
    }
    // 合并改變 并且發(fā)布
    Release release = releaseService.mergeBranchChangeSetsAndRelease(namespace, branchName, releaseName,
                                                                     releaseComment, isEmergencyPublish, changeSets);
    // 是否刪除分支
    if (deleteBranch) {
      namespaceBranchService.deleteBranch(appId, clusterName, namespaceName, branchName,
                                          NamespaceBranchStatus.MERGED, changeSets.getDataChangeLastModifiedBy());
    }
    // 保存發(fā)布消息到數(shù)據(jù)庫
    messageSender.sendMessage(ReleaseMessageKeyGenerator.generate(appId, clusterName, namespaceName),
                              Topics.APOLLO_RELEASE_TOPIC);

    return BeanUtils.transfrom(ReleaseDTO.class, release);
  }

這個(gè)接口接受 portal 調(diào)用悦穿,比較有趣的點(diǎn)是攻礼,這里的 changeSet 是 portal 計(jì)算的,而不是 admin 自己計(jì)算的栗柒。

然后礁扮,controller 層比較簡(jiǎn)單,數(shù)據(jù)校驗(yàn)瞬沦,調(diào)用 Service太伊,發(fā)送消息。

當(dāng)然主要看看 Service逛钻。

主要是 releaseService 的 mergeBranchChangeSetsAndRelease 方法僚焦,看名字,任務(wù)很多:合并分支修改集合曙痘,并且發(fā)布芳悲。

代碼如下:

@Transactional
public Release mergeBranchChangeSetsAndRelease(Namespace namespace, String branchName, String releaseName,
                                               String releaseComment, boolean isEmergencyPublish,
                                               ItemChangeSets changeSets) {
  // 檢查鎖
  checkLock(namespace, isEmergencyPublish, changeSets.getDataChangeLastModifiedBy());
  /// 更新 item
  itemSetService.updateSet(namespace, changeSets);
  // 找到最新發(fā)布的 release
  Release branchRelease = findLatestActiveRelease(namespace.getAppId(), branchName, namespace
      .getNamespaceName());
  // release Id
  long branchReleaseId = branchRelease == null ? 0 : branchRelease.getId();
  // 找到當(dāng)前 namespace 的所有 Item(剛剛更新的)
  Map<String, String> operateNamespaceItems = getNamespaceItems(namespace);

  Map<String, Object> operationContext = Maps.newHashMap();
  // 構(gòu)造操作上下文 sourceBranch=灰度名稱 baseReleaseId=最新的releaseId isEmergencyPublish=是否緊急發(fā)布, 用于構(gòu)建發(fā)布?xì)v史
  operationContext.put(ReleaseOperationContext.SOURCE_BRANCH, branchName);
  operationContext.put(ReleaseOperationContext.BASE_RELEASE_ID, branchReleaseId);
  operationContext.put(ReleaseOperationContext.IS_EMERGENCY_PUBLISH, isEmergencyPublish);
  // ReleaseHistory Audit 主版本
  return masterRelease(namespace, releaseName, releaseComment, operateNamespaceItems,
                       changeSets.getDataChangeLastModifiedBy(),
                       // 灰度合并回主分支發(fā)布
                       ReleaseOperation.GRAY_RELEASE_MERGE_TO_MASTER, operationContext);

}

代碼很簡(jiǎn)單,步驟:

  1. 檢查鎖边坤,和普通發(fā)布一樣名扛,判斷修改者和發(fā)布者是不是同一個(gè)人。
  2. 根據(jù) Portal 傳遞來的 changeSets 更新 item茧痒。
  3. 找到最新發(fā)布的 release(構(gòu)建發(fā)布?xì)v史的上下文)肮韧。
  4. 發(fā)布主版本。

其中,updateSet 方法比較重要惹苗,要看看他是怎么更新 item 的殿较。

方法很長(zhǎng),總之桩蓉,就是將 changeSet 的內(nèi)容保存到主版本的 namespace 下淋纲。

@Transactional
public ItemChangeSets updateSet(String appId, String clusterName,
                                String namespaceName, ItemChangeSets changeSet) {
  //  最后改變數(shù)據(jù)的人
  String operator = changeSet.getDataChangeLastModifiedBy();
  // 改變數(shù)據(jù)的詳細(xì)信息
  ConfigChangeContentBuilder configChangeContentBuilder = new ConfigChangeContentBuilder();
  // 如果創(chuàng)建了新的
  if (!CollectionUtils.isEmpty(changeSet.getCreateItems())) {
    // 循環(huán)
    for (ItemDTO item : changeSet.getCreateItems()) {
      // 轉(zhuǎn)換
      Item entity = BeanUtils.transfrom(Item.class, item);
      entity.setDataChangeCreatedBy(operator);
      entity.setDataChangeLastModifiedBy(operator);
      // 保存 item 到數(shù)據(jù)庫
      Item createdItem = itemService.save(entity);
      // 保存到 builder createItems List 中
      configChangeContentBuilder.createItem(createdItem);
    }
    // 最后記錄審核
    auditService.audit("ItemSet", null, Audit.OP.INSERT, operator);
  }
  // 如果有修改的數(shù)據(jù)
  if (!CollectionUtils.isEmpty(changeSet.getUpdateItems())) {
    for (ItemDTO item : changeSet.getUpdateItems()) {
      // 轉(zhuǎn)換并尋找
      Item entity = BeanUtils.transfrom(Item.class, item);
      Item managedItem = itemService.findOne(entity.getId());
      // 不存在拋出異常
      if (managedItem == null) {
        throw new NotFoundException(String.format("item not found.(key=%s)", entity.getKey()));
      }
      // 之前的數(shù)據(jù)
      Item beforeUpdateItem = BeanUtils.transfrom(Item.class, managedItem);

      //protect. only value,comment,lastModifiedBy,lineNum can be modified
      // 將之前數(shù)據(jù)內(nèi)容更新
      managedItem.setValue(entity.getValue());
      managedItem.setComment(entity.getComment());
      managedItem.setLineNum(entity.getLineNum());
      managedItem.setDataChangeLastModifiedBy(operator);
      // 更新
      Item updatedItem = itemService.update(managedItem);
      // 更新 builder 中 value
      configChangeContentBuilder.updateItem(beforeUpdateItem, updatedItem);

    }
    // 最后審核 itemSet
    auditService.audit("ItemSet", null, Audit.OP.UPDATE, operator);
  }
  // 如果有刪除的
  if (!CollectionUtils.isEmpty(changeSet.getDeleteItems())) {
    for (ItemDTO item : changeSet.getDeleteItems()) {
      // 數(shù)據(jù)庫刪除
      Item deletedItem = itemService.delete(item.getId(), operator);
      // 添加到 builder 中
      configChangeContentBuilder.deleteItem(deletedItem);
    }
    // 審核
    auditService.audit("ItemSet", null, Audit.OP.DELETE, operator);
  }
  // 如果 builder 中有內(nèi)容
  if (configChangeContentBuilder.hasContent()){
    // 創(chuàng)建提交記錄
    createCommit(appId, clusterName, namespaceName,
        configChangeContentBuilder.build(), // 將 build 變成 json 保存
                 changeSet.getDataChangeLastModifiedBy());
  }

  return changeSet;
}

在成功更新 itme 之后,便可以進(jìn)行最終的發(fā)布了院究,發(fā)布很簡(jiǎn)單洽瞬,就不展開講了。

然后看看刪除灰度业汰,默認(rèn)是要?jiǎng)h除的伙窃。

步驟:

  1. 找到灰度發(fā)布的最新 release。
  2. 更新灰度規(guī)則样漆,置空灰度規(guī)則为障。
  3. 刪除灰度 cluster 和關(guān)聯(lián)的 namespace。置于灰度為什么和 cluster 關(guān)聯(lián)放祟,而不是和 namespace 關(guān)聯(lián)鳍怨,這是因?yàn)樽畛醯?apollo 沒有設(shè)計(jì)灰度,后面加上灰度的時(shí)候跪妥,為了避免 namespace 大幅修改鞋喇,就在 cluster 里加入父子邏輯了(咨詢過作者)。
  4. 記錄發(fā)布?xì)v史眉撵。根據(jù)是否 merge 記錄是放棄灰度還是合并后刪除侦香,方便審計(jì)。

發(fā)布操作有很多類型纽疟,apollo 的常量如下:

public interface ReleaseOperation {
  int NORMAL_RELEASE = 0;//普通發(fā)布
  int ROLLBACK = 1;// 回滾
  int GRAY_RELEASE = 2;// 灰度發(fā)布
  int APPLY_GRAY_RULES = 3;// 灰度規(guī)則更新
  int GRAY_RELEASE_MERGE_TO_MASTER = 4;// 灰度合并回主分支發(fā)布
  int MASTER_NORMAL_RELEASE_MERGE_TO_GRAY = 5;// 主分支發(fā)布灰度自動(dòng)發(fā)布
  int MATER_ROLLBACK_MERGE_TO_GRAY = 6;// 主分支回滾灰度自動(dòng)發(fā)布
  int ABANDON_GRAY_RELEASE = 7;//放棄灰度
  int GRAY_RELEASE_DELETED_AFTER_MERGE = 8;// 灰度版本合并后刪除
}

總結(jié)一下 admin 的發(fā)布流程:

4. 總結(jié)

將 portal 和 admin 組合起來看罐韩,下圖:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仰挣,隨后出現(xiàn)的幾起案子伴逸,更是在濱河造成了極大的恐慌缠沈,老刑警劉巖膘壶,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異洲愤,居然都是意外死亡颓芭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門柬赐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亡问,“玉大人,你說我怎么就攤上這事≈菖海” “怎么了束世?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)床玻。 經(jīng)常有香客問我毁涉,道長(zhǎng),這世上最難降的妖魔是什么锈死? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任贫堰,我火速辦了婚禮,結(jié)果婚禮上待牵,老公的妹妹穿的比我還像新娘其屏。我一直安慰自己,他們只是感情好缨该,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布偎行。 她就那樣靜靜地躺著,像睡著了一般贰拿。 火紅的嫁衣襯著肌膚如雪睦优。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天壮不,我揣著相機(jī)與錄音汗盘,去河邊找鬼。 笑死询一,一個(gè)胖子當(dāng)著我的面吹牛隐孽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播健蕊,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼菱阵,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了缩功?” 一聲冷哼從身側(cè)響起晴及,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嫡锌,沒想到半個(gè)月后虑稼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡势木,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年蛛倦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啦桌。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡溯壶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情且改,我是刑警寧澤验烧,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站又跛,受9級(jí)特大地震影響噪窘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜效扫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一倔监、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菌仁,春花似錦浩习、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摹迷,卻和暖如春疟赊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背峡碉。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工近哟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鲫寄。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓吉执,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親地来。 傳聞我的和親對(duì)象是個(gè)殘疾皇子戳玫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 目錄 Controller 層 Service 層 publish 方法 發(fā)送 ReleaseMessage 消息...
    莫那一魯?shù)?/span>閱讀 1,245評(píng)論 0 1
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,898評(píng)論 2 89
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)未斑,斷路器咕宿,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 北宋年間载城。隆冬時(shí)節(jié)肌似,寒風(fēng)凜冽。陽谷縣城诉瓦,街頭行人寥落。 武大郎把燒餅挑子挪到避風(fēng)的墻角處,雙手插在袖筒里睬澡,雙腳不停...
    輕讀漫寫閱讀 671評(píng)論 0 4
  • 滴答滴答固额,時(shí)間不停的在轉(zhuǎn)動(dòng),忽然耳邊傳來了吱吱喳喳的鳥鳴聲煞聪,抬眼一看斗躏,原來是那只被我“包養(yǎng)”的鵲兒停在窗外面的樹...
    HZ新鮮感閱讀 291評(píng)論 0 0