系统推送的集成(二十二) —— 关于本地通知的详细解析(二)

版本记录

版本号 时间
V1.0 2021.05.24 星期一

前言

我们做APP很多时候都需要推送功能,以直播为例,如果你关注的主播开播了,那么就需要向关注这个主播的人发送开播通知,提醒用户去看播,这个只是一个小的方面,具体应用根据公司的业务逻辑而定。前面已经花了很多篇幅介绍了极光推送,其实极光推送无非就是将我们客户端和服务端做的很多东西封装了一下,节省了我们很多处理逻辑和流程,这一篇开始,我们就利用系统的原生推送类结合工程实践说一下系统推送的集成,希望我的讲解能让大家很清楚的理解它。感兴趣的可以看上面几篇。
1. 系统推送的集成(一) —— 基本集成流程(一)
2. 系统推送的集成(二) —— 推送遇到的几个坑之BadDeviceToken问题(一)
3. 系统推送的集成(三) —— 本地和远程通知编程指南之你的App的通知 - 本地和远程通知概览(一)
4. 系统推送的集成(四) —— 本地和远程通知编程指南之你的App的通知 - 管理您的应用程序的通知支持(二)
5. 系统推送的集成(五) —— 本地和远程通知编程指南之你的App的通知 - 调度和处理本地通知(三)
6. 系统推送的集成(六) —— 本地和远程通知编程指南之你的App的通知 - 配置远程通知支持(四)
7. 系统推送的集成(七) —— 本地和远程通知编程指南之你的App的通知 - 修改和显示通知(五)
8. 系统推送的集成(八) —— 本地和远程通知编程指南之苹果推送通知服务APNs - APNs概览(一)
9. 系统推送的集成(九) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 创建远程通知Payload(二)
10. 系统推送的集成(十) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 与APNs通信(三)
11. 系统推送的集成(十一) —— 本地和远程通知编程指南之苹果推送通知服务APNs - Payload Key参考(四)
12. 系统推送的集成(十二) —— 本地和远程通知编程指南之Legacy信息 - 二进制Provider API(一)
13. 系统推送的集成(十三) —— 本地和远程通知编程指南之Legacy信息 - Legacy通知格式(二)
14. 系统推送的集成(十四) —— 发送和处理推送通知流程详解(一)
15. 系统推送的集成(十五) —— 发送和处理推送通知流程详解(二)
16. 系统推送的集成(十六) —— 自定义远程通知(一)
17. 系统推送的集成(十七) —— APNs从工程配置到自定义通知UI全流程解析(一)
18. 系统推送的集成(十八) —— APNs从工程配置到自定义通知UI全流程解析(二)
19. 系统推送的集成(十九) —— APNs配置接收和处理的简单入门(一)
20. 系统推送的集成(二十) —— APNs配置接收和处理的简单入门(二)
21. 系统推送的集成(二十一) —— 关于本地通知的详细解析(一)

源码

1. Swift

首先看下工程组织结构

下面就是源码啦

1. TaskManager.swift
import Foundation

class TaskManager: ObservableObject {
  static let shared = TaskManager()
  let taskPersistenceManager = TaskPersistenceManager()

  @Published var tasks: [Task] = []

  init() {
    loadTasks()
  }

  func save(task: Task) {
    tasks.append(task)
    DispatchQueue.global().async {
      self.taskPersistenceManager.save(tasks: self.tasks)
    }
    if task.reminderEnabled {
      NotificationManager.shared.scheduleNotification(task: task)
    }
  }

  func loadTasks() {
    self.tasks = taskPersistenceManager.loadTasks()
  }

  func addNewTask(_ taskName: String, _ reminder: Reminder?) {
    if let reminder = reminder {
      save(task: Task(name: taskName, reminderEnabled: true, reminder: reminder))
    } else {
      save(task: Task(name: taskName, reminderEnabled: false, reminder: Reminder()))
    }
  }

  func remove(task: Task) {
    tasks.removeAll {
      $0.id == task.id
    }
    DispatchQueue.global().async {
      self.taskPersistenceManager.save(tasks: self.tasks)
    }
    if task.reminderEnabled {
      NotificationManager.shared.removeScheduledNotification(task: task)
    }
  }

  func markTaskComplete(task: Task) {
    if let row = tasks.firstIndex(where: { $0.id == task.id }) {
      var updatedTask = task
      updatedTask.completed = true
      tasks[row] = updatedTask
    }
  }
}
2. NotificationManager.swift
import Foundation
import UserNotifications
import CoreLocation

enum NotificationManagerConstants {
  static let timeBasedNotificationThreadId =
    "TimeBasedNotificationThreadId"
  static let calendarBasedNotificationThreadId =
    "CalendarBasedNotificationThreadId"
  static let locationBasedNotificationThreadId =
    "LocationBasedNotificationThreadId"
}

class NotificationManager: ObservableObject {
  static let shared = NotificationManager()
  @Published var settings: UNNotificationSettings?

  func requestAuthorization(completion: @escaping  (Bool) -> Void) {
    UNUserNotificationCenter.current()
      .requestAuthorization(options: [.alert, .sound, .badge]) { granted, _  in
        self.fetchNotificationSettings()
        completion(granted)
      }
  }

  func fetchNotificationSettings() {
    // 1
    UNUserNotificationCenter.current().getNotificationSettings { settings in
      // 2
      DispatchQueue.main.async {
        self.settings = settings
      }
    }
  }

  func removeScheduledNotification(task: Task) {
    UNUserNotificationCenter.current()
      .removePendingNotificationRequests(withIdentifiers: [task.id])
  }

  // 1
  func scheduleNotification(task: Task) {
    // 2
    let content = UNMutableNotificationContent()
    content.title = task.name
    content.body = "Gentle reminder for your task!"
    content.categoryIdentifier = "OrganizerPlusCategory"
    let taskData = try? JSONEncoder().encode(task)
    if let taskData = taskData {
      content.userInfo = ["Task": taskData]
    }

    // 3
    var trigger: UNNotificationTrigger?
    switch task.reminder.reminderType {
    case .time:
      if let timeInterval = task.reminder.timeInterval {
        trigger = UNTimeIntervalNotificationTrigger(
          timeInterval: timeInterval,
          repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.timeBasedNotificationThreadId
    case .calendar:
      if let date = task.reminder.date {
        trigger = UNCalendarNotificationTrigger(
          dateMatching: Calendar.current.dateComponents(
            [.day, .month, .year, .hour, .minute],
            from: date),
          repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.calendarBasedNotificationThreadId
    case .location:
      // 1
      guard CLLocationManager().authorizationStatus == .authorizedWhenInUse else {
        return
      }
      // 2
      if let location = task.reminder.location {
        // 3
        let center = CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude)
        let region = CLCircularRegion(center: center, radius: location.radius, identifier: task.id)
        trigger = UNLocationNotificationTrigger(region: region, repeats: task.reminder.repeats)
      }
      content.threadIdentifier =
        NotificationManagerConstants.locationBasedNotificationThreadId
    }

    // 4
    if let trigger = trigger {
      let request = UNNotificationRequest(
        identifier: task.id,
        content: content,
        trigger: trigger)
      // 5
      UNUserNotificationCenter.current().add(request) { error in
        if let error = error {
          print(error)
        }
      }
    }
  }
}
3. TaskPersistenceManager.swift
import Foundation

class TaskPersistenceManager {
  enum FileConstants {
    static let tasksFileName = "tasks.json"
  }

  func save(tasks: [Task]) {
    do {
      let documentsDirectory = getDocumentsDirectory()
      let storageURL = documentsDirectory.appendingPathComponent(FileConstants.tasksFileName)
      let tasksData = try JSONEncoder().encode(tasks)
      do {
        try tasksData.write(to: storageURL)
      } catch {
        print("Couldn't write to File Storage")
      }
    } catch {
      print("Couldn't encode tasks data")
    }
  }

  func loadTasks() -> [Task] {
    let documentsDirectory = getDocumentsDirectory()
    let storageURL = documentsDirectory.appendingPathComponent(FileConstants.tasksFileName)
    guard
      let taskData = try? Data(contentsOf: storageURL),
      let tasks = try? JSONDecoder().decode([Task].self, from: taskData)
    else {
      return []
    }

    return tasks
  }

  func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
  }
}
4. LocationManager.swift
import CoreLocation

class LocationManager: NSObject, ObservableObject {
  var locationManager = CLLocationManager()
  @Published var authorized = false

  override init() {
    super.init()
    locationManager.delegate = self
    if locationManager.authorizationStatus == .authorizedWhenInUse {
      authorized = true
      locationManager.startMonitoringSignificantLocationChanges()
    }
  }

  func requestAuthorization() {
    locationManager.requestWhenInUseAuthorization()
  }
}

// MARK: - CLLocationManagerDelegate
extension LocationManager: CLLocationManagerDelegate {
  func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    if locationManager.authorizationStatus == .authorizedWhenInUse ||
      locationManager.authorizationStatus == .authorizedAlways {
      authorized = true
    } else {
      authorized = false
    }
  }
}`
5. TaskListView.swift
import SwiftUI

struct TaskListView: View {
  @ObservedObject var taskManager = TaskManager.shared
  @State var showNotificationSettingsUI = false

  var body: some View {
    ZStack {
      VStack {
        HStack {
          Spacer()
          Text("Organizer Plus")
            .font(.title)
            .foregroundColor(.pink)
          Spacer()
          Button(
            action: {
              // 1
              NotificationManager.shared.requestAuthorization { granted in
                // 2
                if granted {
                  showNotificationSettingsUI = true
                }
              }
            },
            label: {
              Image(systemName: "bell")
                .font(.title)
                .accentColor(.pink)
            })
            .padding(.trailing)
            .sheet(isPresented: $showNotificationSettingsUI) {
              NotificationSettingsView()
            }
        }
        .padding()
        if taskManager.tasks.isEmpty {
          Spacer()
          Text("No Tasks!")
            .foregroundColor(.pink)
            .font(.title3)
          Spacer()
        } else {
          List(taskManager.tasks) { task in
            TaskCell(task: task)
          }
          .padding()
        }
      }
      AddTaskView()
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    TaskListView()
  }
}

struct TaskCell: View {
  var task: Task

  var body: some View {
    HStack {
      Button(
        action: {
          TaskManager.shared.markTaskComplete(task: task)
          DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
            TaskManager.shared.remove(task: task)
          }
        }, label: {
          Image(systemName: task.completed ? "checkmark.circle.fill" : "circle")
            .resizable()
            .frame(width: 20, height: 20)
            .accentColor(.pink)
        })
      if task.completed {
        Text(task.name)
          .strikethrough()
          .foregroundColor(.pink)
      } else {
        Text(task.name)
          .foregroundColor(.pink)
      }
    }
  }
}

struct AddTaskView: View {
  @State var showCreateTaskView = false

  var body: some View {
    VStack {
      Spacer()
      HStack {
        Spacer()
        Button(
          action: {
            showCreateTaskView = true
          }, label: {
            Text("+")
              .font(.largeTitle)
              .multilineTextAlignment(.center)
              .frame(width: 30, height: 30)
              .foregroundColor(Color.white)
              .padding()
          })
          .background(Color.pink)
          .cornerRadius(40)
          .padding()
          .sheet(isPresented: $showCreateTaskView) {
            CreateTaskView()
          }
      }
      .padding(.bottom)
    }
  }
}
6. CreateTaskView.swift
import SwiftUI
import MapKit

struct CreateTaskView: View {
  @State var taskName: String = ""
  @State var reminderEnabled = false
  @State var selectedTrigger = ReminderType.time
  @State var timeDurationIndex: Int = 0
  @State private var dateTrigger = Date()
  @State private var shouldRepeat = false
  @State private var latitude: String = ""
  @State private var longitude: String = ""
  @State private var radius: String = ""
  @Environment(\.presentationMode) var presentationMode

  let triggers = ["Time", "Calendar", "Location"]
  let timeDurations: [Int] = Array(1...59)
  var body: some View {
    NavigationView {
      Form {
        Section {
          HStack {
            Spacer()
            Text("Add Task")
              .font(.title)
              .padding()
            Spacer()
            Button("Save") {
              TaskManager.shared.addNewTask(taskName, makeReminder())
              presentationMode.wrappedValue.dismiss()
            }
            .disabled(taskName.isEmpty ? true : false)
            .padding()
          }
          VStack {
            TextField("Enter name for the task", text: $taskName)
              .padding(.vertical)
            Toggle(isOn: $reminderEnabled) {
              Text("Add Reminder")
            }
            .padding(.vertical)

            if reminderEnabled {
              ReminderView(
                selectedTrigger: $selectedTrigger,
                timeDurationIndex: $timeDurationIndex,
                triggerDate: $dateTrigger,
                shouldRepeat: $shouldRepeat,
                latitude: $latitude,
                longitude: $longitude,
                radius: $radius)
                .navigationBarHidden(true)
                .navigationTitle("")
            }
            Spacer()
          }
          .padding()
        }
      }
      .navigationBarTitle("")
      .navigationBarHidden(true)
    }
  }

  func makeReminder() -> Reminder? {
    guard reminderEnabled else {
      return nil
    }
    var reminder = Reminder()
    reminder.reminderType = selectedTrigger
    switch selectedTrigger {
    case .time:
      reminder.timeInterval = TimeInterval(timeDurations[timeDurationIndex] * 60)
    case .calendar:
      reminder.date = dateTrigger
    case .location:
      if let latitude = Double(latitude),
        let longitude = Double(longitude),
        let radius = Double(radius) {
        reminder.location = LocationReminder(
          latitude: latitude,
          longitude: longitude,
          radius: radius)
      }
    }
    reminder.repeats = shouldRepeat
    return reminder
  }
}

struct CreateTaskView_Previews: PreviewProvider {
  static var previews: some View {
    CreateTaskView()
  }
}

struct ReminderView: View {
  @Binding var selectedTrigger: ReminderType
  @Binding var timeDurationIndex: Int
  @Binding var triggerDate: Date
  @Binding var shouldRepeat: Bool
  @Binding var latitude: String
  @Binding var longitude: String
  @Binding var radius: String
  @StateObject var locationManager = LocationManager()

  var body: some View {
    VStack {
      Picker("Notification Trigger", selection: $selectedTrigger) {
        Text("Time").tag(ReminderType.time)
        Text("Date").tag(ReminderType.calendar)
        Text("Location").tag(ReminderType.location)
      }
      .pickerStyle(SegmentedPickerStyle())
      .padding(.vertical)
      if selectedTrigger == ReminderType.time {
        Picker("Time Interval", selection: $timeDurationIndex) {
          ForEach(1 ..< 59) { i in
            if i == 1 {
              Text("\(i) minute").tag(i)
            } else {
              Text("\(i) minutes").tag(i)
            }
          }
          .navigationBarHidden(true)
          .padding(.vertical)
        }
      } else if selectedTrigger == ReminderType.calendar {
        DatePicker("Please enter a date", selection: $triggerDate)
          .labelsHidden()
          .padding(.vertical)
      } else {
        VStack {
          if !locationManager.authorized {
            Button(
              action: {
                locationManager.requestAuthorization()
              },
              label: {
                Text("Request Location Authorization")
              })
          } else {
            TextField("Enter Latitude", text: $latitude)
            TextField("Enter Longitude", text: $longitude)
            TextField("Enter Radius", text: $radius)
          }
        }
        .padding(.vertical)
      }
      Toggle(isOn: $shouldRepeat) {
        Text("Repeat Notification")
      }
    }
  }
}
7. NotificationSettingsView.swift
import SwiftUI

struct NotificationSettingsView: View {
  @ObservedObject var notificationManager = NotificationManager.shared

  var body: some View {
    VStack {
      Form {
        Section {
          HStack {
            Spacer()
            Text("Notification Settings")
              .font(.title2)
            Spacer()
          }
        }
        Section {
          SettingRowView(
            setting: "Authorization Status",
            enabled: notificationManager.settings?.authorizationStatus == UNAuthorizationStatus.authorized)
          SettingRowView(
            setting: "Show in Notification Center",
            enabled: notificationManager.settings?.notificationCenterSetting == .enabled)
          SettingRowView(
            setting: "Sound Enabled?",
            enabled: notificationManager.settings?.soundSetting == .enabled)
          SettingRowView(
            setting: "Badges Enabled?",
            enabled: notificationManager.settings?.badgeSetting == .enabled)
          SettingRowView(
            setting: "Alerts Enabled?",
            enabled: notificationManager.settings?.alertSetting == .enabled)
          SettingRowView(
            setting: "Show on lock screen?",
            enabled: notificationManager.settings?.lockScreenSetting == .enabled)
          SettingRowView(
            setting: "Alert banners?",
            enabled: notificationManager.settings?.alertStyle == .banner)
          SettingRowView(
            setting: "Critical Alerts?",
            enabled: notificationManager.settings?.criticalAlertSetting == .enabled)
          SettingRowView(
            setting: "Siri Announcement?",
            enabled: notificationManager.settings?.announcementSetting == .enabled)
        }
      }
    }
  }
}

struct NotificationSettingsView_Previews: PreviewProvider {
  static var previews: some View {
    NotificationSettingsView()
  }
}

struct SettingRowView: View {
  var setting: String
  var enabled: Bool
  var body: some View {
    HStack {
      Text(setting)
      Spacer()
      if enabled {
        Image(systemName: "checkmark")
          .foregroundColor(.green)
      } else {
        Image(systemName: "xmark")
          .foregroundColor(.red)
      }
    }
    .padding()
  }
}
8. Task.swift
import Foundation

struct Task: Identifiable, Codable {
  var id = UUID().uuidString
  var name: String
  var completed = false
  var reminderEnabled = false
  var reminder: Reminder
}

enum ReminderType: Int, CaseIterable, Identifiable, Codable {
  case time
  case calendar
  case location
  var id: Int { self.rawValue }
}

struct Reminder: Codable {
  var timeInterval: TimeInterval?
  var date: Date?
  var location: LocationReminder?
  var reminderType: ReminderType = .time
  var repeats = false
}

struct LocationReminder: Codable {
  var latitude: Double
  var longitude: Double
  var radius: Double
}
9. AppMain.swift
import SwiftUI

@main
struct AppMain: App {
  @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

  var body: some Scene {
    WindowGroup {
      TaskListView()
    }
  }
}`
10. AppDelegate.swift
import UIKit

class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
  ) -> Bool {
    configureUserNotifications()
    return true
  }
}

// MARK: - UNUserNotificationCenterDelegate
extension AppDelegate: UNUserNotificationCenterDelegate {
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void
  ) {
    completionHandler(.banner)
  }

  private func configureUserNotifications() {
    UNUserNotificationCenter.current().delegate = self
    // 1
    let dismissAction = UNNotificationAction(
      identifier: "dismiss",
      title: "Dismiss",
      options: []
    )
    let markAsDone = UNNotificationAction(
      identifier: "markAsDone",
      title: "Mark As Done",
      options: []
    )
    // 2
    let category = UNNotificationCategory(
      identifier: "OrganizerPlusCategory",
      actions: [dismissAction, markAsDone],
      intentIdentifiers: [],
      options: []
    )
    // 3
    UNUserNotificationCenter.current().setNotificationCategories([category])
  }

  // 1
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {
    // 2
    if response.actionIdentifier == "markAsDone" {
      let userInfo = response.notification.request.content.userInfo
      if let taskData = userInfo["Task"] as? Data {
        if let task = try? JSONDecoder().decode(Task.self, from: taskData) {
          // 3
          TaskManager.shared.remove(task: task)
        }
      }
    }
    completionHandler()
  }
}

后记

本篇主要讲述了关于本地通知的详细解析,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容