Core NFC框架详细解析 (二) —— CoreNFC使用简单示例(一)

版本号 时间
V1.0 2020.06.07 星期日

前言

今天翻阅苹果的API文档,发现多了一个框架Core NFC,看了下才看见是iOS11.0新添加的框架,这里我们就一起来看一下框架Core NFC。感兴趣的看下面几篇文章。
1. Core NFC框架详细解析 (一) —— 基本概览(一)

开始

首先看下主要内容:

在本教程中,您将学习如何使用CoreNFC无线连接到其他设备或NFC标签。内容来自翻译

下面看下写作环境:

Swift 5, iOS 13, Xcode 11

Near Field Communication(NFC)是一种用于短距离无线设备与其他设备共享数据或触发这些设备上的动作的技术。它使用射频场(radio frequency field)进行构建,它可以使没有电源的设备存储小块数据,同时还使其他有源设备可以读取该数据。

iOSwatchOS设备已经内置了NFC硬件已有几年了。实际上,Apple Pay使用此技术与商店中的支付终端进行交互。但是,开发人员要到iOS 11才能使用NFC硬件。

苹果通过引入Core NFC提升了iOS 13中的NFC游戏。借助这项新技术,您可以对iOS设备进行编程,使其以新方式与周围的互联世界互动。本教程将向您展示一些使用该技术的方法。在此过程中,您将学习如何:

  • 将标准信息写入NFC tag标签。
  • 阅读该信息。
  • 将自定义信息保存到标签tag
  • 修改标签上已经找到的数据。

重要说明:要执行本教程中的所有步骤,您需要满足以下条件:

  • 物理iOS设备
  • 苹果开发者帐户
  • 您可以读取和写入的NFC硬件。许多在线零售商都以合理的价格携带NFC tag。通常,您可以以大约10美元的价格获得一包NFC标签。在说明中寻找表明它是可编程的或列出其存储容量(通常为300500字节)的内容。具有该近似容量的任何设备都超出了本教程的范围。

starter文件夹中打开starter项目。 使用项目应用程序,您将学习如何:

  • NFC tag设置为“location”
  • 扫描location tag以查看其名称和访客日志。
  • 将访问者(visitor)添加到位置标签(location tag)。

构建并运行。 您会看到以下内容:


Writing to Your First Tag

首先,在Project navigator中选择NeatoCache项目。 然后,转到Signing & Capability,然后选择+ Capability。 从列表中选择Near Field Communication Tag Reading

这将确保您的应用程序的配置文件(provisioning profile)设置为使用NFC

接下来,打开您的Info.plist并添加以下条目:

  • KeyPrivacy – NFC Scan Usage Description
  • Value:使用NFC读取和写入数据

您需要此条目来向用户传达您正在使用NFC功能的用途,并符合Apple关于在应用程序中使用NFC的要求。

接下来,您将添加一个函数,该函数可以执行您的应用将处理的各种NFC任务。 打开NFCUtility.swift并将以下导入和类型别名添加到文件顶部:

import CoreNFC

typealias NFCReadingCompletion = (Result<NFCNDEFMessage?, Error>) -> Void
typealias LocationReadingCompletion = (Result<Location, Error>) -> Void

您需要导入CoreNFC才能使用NFC。 类型别名(type aliases)提供以下功能:

  • NFCReadingCompletion用于完成通用标签读取任务。
  • LocationReadingCompletion,用于读取配置为位置的标签

接下来,将以下属性和方法添加到NFCUtility

// 1
private var session: NFCNDEFReaderSession?
private var completion: LocationReadingCompletion?

// 2
static func performAction(
  _ action: NFCAction,
  completion: LocationReadingCompletion? = nil
) {
  // 3
  guard NFCNDEFReaderSession.readingAvailable else {
    completion?(.failure(NFCError.unavailable))
    print("NFC is not available on this device")
    return
  }

  shared.action = action
  shared.completion = completion
  // 4
  shared.session = NFCNDEFReaderSession(
    delegate: shared.self,
    queue: nil,
    invalidateAfterFirstRead: false)
  // 5
  shared.session?.alertMessage = action.alertMessage
  // 6
  shared.session?.begin()
}

如果您由于不符合NFCNDEFReaderSessionDelegate而此时遇到编译错误,请不要担心,您将立即修复此问题。

这是您刚刚做的:

  • 1) 您添加sessioncompletion属性以存储活动的NFC reading session及其完成块(completion block)
  • 2) 添加静态函数作为NFC读写任务的入口点。通常,您将使用单例样式访问此函数和NFCUtility
  • 3) 确保设备支持NFC读取。否则,请返回errorcomplete
  • 4) 创建一个NFCNDEFReaderSession,它代表活动的阅读会话。您还可以设置代表以通知NFC阅读会话的各种事件。
  • 5) 您可以在会话上设置alertMessage属性,以使其在NFC模式下向用户显示该文本。
  • 6) 开始阅读会话。调用时,模态将向用户呈现您在上一步中设置的所有指令。

1. Understanding NDEF

请注意,上面的代码引入了另一个首字母缩写词NDEF,代表NFC Data Exchange Format。这是用于写入或读取NFC设备的标准格式。您将使用两种NDEF:

  • NDEF Record:其中包含您的有效载荷(payload)值,例如字符串,URL或自定义数据。它还包含有关该有效负载值的信息,例如长度和类型。此信息是CoreNFC中的NFCNDEFPayload
  • NDEF Message:这是保存NDEF记录的数据结构。 NDEF消息中可以有一个或多个NDEF记录。

2. Detecting Tags

现在,您已经设置了NFCReaderSession,现在遵循NFCUtility成为代理了,这样就可以通知您在读取会话期间发生的各种事件。

将以下代码添加到NFCUtility.swift的底部:

// MARK: - NFC NDEF Reader Session Delegate
extension NFCUtility: NFCNDEFReaderSessionDelegate {
  func readerSession(
    _ session: NFCNDEFReaderSession,
    didDetectNDEFs messages: [NFCNDEFMessage]
  ) {
    // Not used
  }
}

您将在一秒钟内向此扩展添加更多内容,但请注意,在本教程中,您将不会对readerSession(_:didDetectNDEFs :)进行任何操作。 您仅在此处添加它,因为必须遵守委托协议。

NFC技术的互动越多,您越会发现在读写过程的各个阶段遇到错误的可能性。 将以下方法添加到新扩展中以捕获这些错误:

private func handleError(_ error: Error) {
  session?.alertMessage = error.localizedDescription
  session?.invalidate()
}

代码的第一行应该看起来很熟悉。 它将在NFC模式视图中向用户显示错误消息。 如果发生错误,您还将使会话无效以终止会话并允许用户再次与该应用进行交互。

接下来,将以下方法添加到扩展中以处理NFC读取会话中的错误:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didInvalidateWithError error: Error
) {
  if let error = error as? NFCReaderError,
    error.code != .readerSessionInvalidationErrorFirstNDEFTagRead &&
      error.code != .readerSessionInvalidationErrorUserCanceled {
    completion?(.failure(NFCError.invalidated(message: 
      error.localizedDescription)))
  }

  self.session = nil
  completion = nil
}

添加此委托方法将清除到目前为止您遇到的所有编译错误。

最后,将最后一种方法添加到您的扩展中,以处理可能的NFC标签检测:

func readerSession(
  _ session: NFCNDEFReaderSession,
  didDetect tags: [NFCNDEFTag]
) {
  guard 
    let tag = tags.first,
    tags.count == 1 
    else {
      session.alertMessage = """
        There are too many tags present. Remove all and then try again.
        """
      DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) {
        session.restartPolling()
      }
      return
  }
}

在这里,您可以实现在会话检测到您扫描标签时将调用的方法。

通常,您希望用户只有一个标签离手机足够近,但是您应该考虑多个标签。 如果检测到此情况,则将停止扫描并alert给用户。 显示该消息后,您将重新启动阅读会话,并让您的用户再试一次。

3. Handling the Tag

知道有一个标签后,您可能想对它做些事情。 在readerSession(_:didDetect :)中的guard声明之后添加以下代码:

// 1
session.connect(to: tag) { error in
  if let error = error {
    self.handleError(error)
    return
  }

  // 2
  tag.queryNDEFStatus { status, _, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 3
    switch (status, self.action) {
    case (.notSupported, _):
      session.alertMessage = "Unsupported tag."
      session.invalidate()
    case (.readOnly, _):
      session.alertMessage = "Unable to write to tag."
      session.invalidate()
    case (.readWrite, .setupLocation(let locationName)):
      self.createLocation(name: locationName, with: tag)
    case (.readWrite, .readLocation):
      return
    default:
      return
    }
  }
}

您在上面的代码中正在做的事情是:

  • 1) 使用当前的NCFNDEFReaderSession连接到检测到的标签。您需要执行此步骤以执行对标签的任何读取或写入。连接后,它将调用其完成处理程序,并可能发生任何错误。
  • 2) 在标签中查询其NDEF状态,以查看是否支持NFC设备。就您的NeatoCache应用而言,状态必须为readWrite
  • 3) 切换状态和NFC操作,并根据其值确定应执行的操作。在这里,您尝试使用createLocation(name:with :)将标签设置为具有位置名称,该名称尚不存在,因此会遇到编译错误。不用担心,您稍后会添加它。同样,readLocation操作也尚未处理。

4. Creating the Payload

到目前为止,您已经在寻找标签,连接标签并查询其状态。要完成对标签的写入设置,请在NFCUtility.swift的末尾添加以下代码块:

// MARK: - Utilities
extension NFCUtility {
  func createLocation(name: String, with tag: NFCNDEFTag) {
    // 1
    guard let payload = NFCNDEFPayload
      .wellKnownTypeTextPayload(string: name, locale: Locale.current) 
      else {
        handleError(NFCError.invalidated(message: "Could not create payload"))
        return
    }

    // 2
    let message = NFCNDEFMessage(records: [payload])

    // 3
    tag.writeNDEF(message) { error in
      if let error = error {
        self.handleError(error)
        return
      }

      self.session?.alertMessage = "Wrote location data."
      self.session?.invalidate()
      self.completion?(.success(Location(name: name)))
    }
  }
}

您在上面的代码中正在做的事情是:

  • 1) 创建文本NFCNDEFPayload。如前所述,这类似于NDEF记录。
  • 2) 使用有效负载创建新的NFCNDEFMessage,以便可以将其保存到NFC设备。
  • 3) 最后,将消息写入标签。

5. Using NDEF Payload Types

NFCNDEFPayload支持几种不同类型的数据。在此示例中,您使用的是wellKnownTypeTextPayload(string:locale :)。这是一种非常简单的数据类型,它使用字符串和设备的当前语言环境。其他一些数据类型包含更复杂的信息。完整清单如下:

  • Empty
  • Well-Known
  • MIME media-type
  • Absolute URI
  • External
  • Unknown
  • Unchanged
  • Reserved

注意:本教程涵盖了Well-KnownUnknown。要了解其他类型,请查看本教程末尾列出的链接。

另请注意,类型可以具有子类型。例如,Well-known的具有TextURI的子类型。

您真的很接近!剩下的就是将用户界面连接到新代码。转到AdminView.swift并替换以下代码:

Button(action: {
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

使用

Button(action: {
  NFCUtility.performAction(.setupLocation(locationName: self.locationName)) { _ in
    self.locationName = ""
  }
}) {
  Text("Save Location…")
}
.disabled(locationName.isEmpty)

这将进行调用,以使用在text field中找到的文本来设置您的位置。

构建并运行,切换到应用程序的Admin选项卡,输入名称并选择Save Location…

您会看到以下内容:

注意:请记住,您需要使用物理设备并具有支持写入功能的NFC标签。

将手机放在NFC标签上后,您会看到一条消息,说明您的位置已成功保存。

6. Reading the Tag

很好! 现在,您已经有了一个可以在标签中写入字符串的应用程序,您就可以为读取标签提供支持。 返回NFCUtility.swift并在readerSession(_:didDetect :)中找到以下代码。

case (.readWrite, .readLocation):
  return

现在,替换它使用下面:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

是时候实现该readLocation(from :)方法了。 将以下内容添加到包含createLocation(name:with :)Utilities扩展中:

func readLocation(from tag: NFCNDEFTag) {
  // 1
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }
    // 2
    guard 
      let message = message,
      let location = Location(message: message) 
      else {
        self.session?.alertMessage = "Could not read tag data."
        self.session?.invalidate()
        return
    }
    self.completion?(.success(location))
    self.session?.alertMessage = "Read tag."
    self.session?.invalidate()
  }
}

您对添加的内容应该有点熟悉,因为它与您写入标签的方式非常相似。

  • 1) 首先,您开始读取标签。 如果可以读取,它将返回找到的所有消息。
  • 2) 接下来,如果有,尝试从消息数据中创建一个Location。 这使用了一个接受NFCNDEFMessage并将其命名的自定义初始化程序。 如果您感到好奇,可以在LocationModel.swift中找到该初始化程序。

最后,打开VisitorView.swift,并在scanSection中,替换以下代码:

Button(action: {
}) {
  Text("Scan Location Tag…")
}

使用下面

Button(action: {
  NFCUtility.performAction(.readLocation) { location in
    self.locationModel = try? location.get()
  }
}) {
  Text("Scan Location Tag…")
}

您已经准备好从标签中读取数据。 构建并运行。

Visitors选项卡上,点击Scan Location Tag…。 您会在用户界面中看到以下内容以及您的位置名称:


Writing Different Data Types

尽管在某些情况下写字符串可能会完美地工作,但您可能会发现想要将其他类型的数据写到标签中。

为此,请在Utilities扩展中的NFCUtility.swift中添加以下内容:

private func read(
  tag: NFCNDEFTag,
  alertMessage: String = "Tag Read",
  readCompletion: NFCReadingCompletion? = nil
) {
  tag.readNDEF { message, error in
    if let error = error {
      self.handleError(error)
      return
    }

    // 1
    if let readCompletion = readCompletion,
       let message = message {
      readCompletion(.success(message))
    } else if 
      let message = message,
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) {
      // 2
      self.completion?(.success(location))
      self.session?.alertMessage = alertMessage
      self.session?.invalidate()
    } else {
      self.session?.alertMessage = "Could not decode tag data."
      self.session?.invalidate()
    }
  }
}

从现在开始,这种读取标签的新方法将成为您大多数活动的切入点。 如您所见,它仍然像以前一样读取标签。 但是,一旦读取标签,它将执行以下两项操作之一:

  • 1) 调用completion handler并将消息传递给它。 这对于将多个NFC任务链接在一起非常有用。
  • 2) 解码有效负载(payload),以便您可以解析标签的记录。 您将稍后再讲到这一点。

1. Writing Custom Data Instead of Strings

此时,您已经准备好将应用程序从编写字符串到标签转换为将自定义数据写入标签。 将以下内容添加到Utilities扩展中:

private func createLocation(_ location: Location, tag: NFCNDEFTag) {
  read(tag: tag) { _ in
    self.updateLocation(location, tag: tag)
  }
}

这是用于创建带有位置标签的新函数。 您可以看到它使用新的read(tag:alsertMessage:readCompletion :)启动该过程,并调用了一个新函数来更新标签上的位置,还调用了一个新的暂时实施updateLocation(_:tag :)方法。

由于您要替换将位置信息写入标签的方式,因此请删除NFCUtility扩展程序开头的createLocation(name:with :),因为不再需要它。 另外,从以下代码在readerSession(_:didDetect :)中更新代码:

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(name: locationName, with: tag)

到下面

case (.readWrite, .setupLocation(let locationName)):
  self.createLocation(Location(name: locationName), tag: tag)

createLocation(_:tag:)后面添加这个方法:

private func updateLocation(
  _ location: Location,
  withVisitor visitor: Visitor? = nil,
  tag: NFCNDEFTag
) {
  // 1
  var alertMessage = "Successfully setup location."
  var tempLocation = location
  
  // 2
  let jsonEncoder = JSONEncoder()
  guard let customData = try? jsonEncoder.encode(tempLocation) else {
    self.handleError(NFCError.invalidated(message: "Bad data"))
    return
  }
  // 3
  let payload = NFCNDEFPayload(
    format: .unknown,
    type: Data(),
    identifier: Data(),
    payload: customData)
  // 4
  let message = NFCNDEFMessage(records: [payload])
}

您在上面的代码中正在做的事情是:

  • 1) 创建默认alert消息和临时位置。 稍后您将回到这些内容。
  • 2) 对传递给函数的Location结构进行编码。 这会将模型转换为原始数据(Data)。 这很重要,因为这是将任何自定义类型写入NFC标签的方式。
  • 3) 创建可以处理数据的有效负载(payload)。 但是,您现在使用unknown作为格式。 这样做时,必须将typeidentifier设置为空数据Data,而有效负载(payload)参数将承载实际的解码模型。
  • 4) 将有效负载添加到新创建的消息中。

总体而言,这似乎与将字符串保存到标签中时没什么不同,只是增加了一个步骤,将Swift数据类型转换为标签可理解的内容。

2. Checking Tag Capacity

要完成将数据写入标签,请在updateLocation(_:withVisitor:tag)中添加下一个代码块:

tag.queryNDEFStatus { _, capacity, _ in
  // 1
  guard message.length <= capacity else {
    self.handleError(NFCError.invalidPayloadSize)
    return
  }

  // 2
  tag.writeNDEF(message) { error in
    if let error = error {
      self.handleError(error)
      return
    }
    
    if self.completion != nil {
      self.read(tag: tag, alertMessage: alertMessage)
    }
  }
}

上面的闭包尝试查询当前NDEF状态,然后:

  • 1) 确保设备有足够的存储空间来存储位置。 请记住,与您可能熟悉的设备相比,NFC标签通常具有极其有限的存储容量。
  • 2) 将消息写入标签。

与上面一样,构建并运行并设置位置。 如果愿意,可以使用之前的相同标签,因为新代码将覆盖以前保存的所有数据。

3. Reading Your Custom Data

此时,如果您尝试读取标签,将会收到错误消息。 保存的数据不再是众所周知的类型。 要解决此问题,请在readerSession(_:didDetect :)中替换以下代码:

case (.readWrite, .readLocation):
  self.readLocation(from: tag)

使用

case (.readWrite, .readLocation):
  self.read(tag: tag)

构建并运行并扫描标签。 因为您在没有任何completion块的情况下调用read(tag:alertMessage:readCompletion :),所以它将解码消息的第一条记录中找到的数据。


Modifying Content

此应用程序的最后一项要求是保存访问此位置的人员的日志。 您的应用具有UI中已经存在的未使用功能,该功能允许用户输入其名称并将其添加到标签中。 到目前为止,您所做的工作将使其余的设置变得微不足道。 您已经可以将数据读取和写入标签,因此对其进行修改应该很容易。

在创建tempLocation之后,在NFCUtility.swift中,将此代码添加到updateLocation(_:withVisitor:tag :)

if let visitor = visitor {
  tempLocation.visitors.append(visitor)
  alertMessage = "Successfully added visitor."
}

在上面的代码中,您检查是否提供了visitor。 如果是这样,则将其添加到该位置的Visitors数组中。

接下来,将以下方法添加到Utilities扩展中:

private func addVisitor(_ visitor: Visitor, tag: NFCNDEFTag) {
  read(tag: tag) { message in
    guard 
      let message = try? message.get(),
      let record = message.records.first,
      let location = try? JSONDecoder()
        .decode(Location.self, from: record.payload) 
      else {
        return
    }

    self.updateLocation(location, withVisitor: visitor, tag: tag)
  }
}

这个新方法将读取一个标签,从中获取消息,并尝试对标签上的Location进行解码。

接下来,在readerSession(_:didDetect :)中,向switch语句添加新的case

case (.readWrite, .addVisitor(let visitorName)):
  self.addVisitor(Visitor(name: visitorName), tag: tag)

如果用户明确想要添加visitor,则将调用在上一步中添加的函数。

剩下的就是更新VisitorView.swift。 在visitorSection中,替换以下代码:

Button(action: {
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

使用

Button(action: {
  NFCUtility
    .performAction(.addVisitor(visitorName: self.visitorName)) { location in
      self.locationModel = try? location.get()
      self.visitorName = ""
    }
}) {
  Text("Add To Tag…")
}
.disabled(visitorName.isEmpty)

Build并运行,然后转到Visitors选项卡。 输入您的姓名,然后选择Add To Tag…。 扫描后,您将看到更新的位置以及在标记上找到的访问者列表。

现在,您应该熟悉Core NFC的基础知识。 框架中还有很多事情没有在本教程中提到。 例如,您可以添加标签的背景阅读,这可以为用户提供一种无需打开应用即可与您的应用进行交互的方式。 如果您使用Apple的Shortcuts应用程序来自动化您的智能家居设备,这对您来说应该很熟悉。 您可以在此处找到有关此操作的更多信息:Adding Support for Background Tag Reading

要了解更多信息,请查看以下一些重要资源:

Apple's Core NFC Documentation是NFC规范中Apple支持的所有内容的首选资源。

NFC Forum Homepage是放置有关NFC所需的所有一般信息及其定义的规范的地方。

后记

本篇主要讲述了CoreNFC使用简单示例,感兴趣的给个赞或者关注~~~

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