【Redis】移動設備離線通知推送全流程實現:系統推送服務與Redis的協同應用

在移動應用開發中,應用未啟動時的通知推送是提升用戶體驗的核心需求之一。當用戶未主動啟動 App 時,如何通過手機通知欄觸達用戶,確保關鍵信息(如訂單提醒、系統警報)不丟失?本文將嘗試解析從 系統推送服務集成消息存儲與喚醒 的全鏈路實現方案,涵蓋 Android(FCM)、iOS(APNs)、Spring Boot 服務端與 Redis 存儲的完整技術棧。


一、核心問題與架構設計

1.1 核心問題定義

需求的核心是:當 App 處于后臺或未啟動狀態時,服務器需將通知推送至手機,通過系統通知欄提示用戶。關鍵挑戰包括:

  • App 離線:無法通過傳統長連接(如 SSE/WebSocket)直接推送。
  • 用戶無感知:需通過系統級通知(通知欄)觸發用戶注意。
  • 消息完整性:用戶打開 App 后需完整查看所有離線通知。

1.2 整體架構設計

方案核心依賴 系統推送服務(觸發通知欄)與 Redis 消息存儲(持久化未讀消息),架構圖如下:

服務器(Spring Boot)系統推送(APNs/FCM)手機( App 未打開)Redis(消息存儲)存儲未讀通知(用戶ID分組)觸發系統推送(設備Token)手機通知欄顯示提醒用戶點擊通知(喚醒App)拉取未讀通知/跳轉目標頁服務器(Spring Boot)系統推送(APNs/FCM)手機( App 未打開)Redis(消息存儲)

二、客戶端集成:獲取設備 Token 與上報

系統推送的前提是獲取設備的唯一標識(Token),Android(FCM)與 iOS(APNs)的 Token 獲取與上報邏輯不同。

2.1 Android:集成 FCM 獲取 Token

FCM(Firebase Cloud Messaging)是 Android 官方推送服務,自動為設備生成唯一 Token。

2.1.1 配置 Firebase 項目
  1. 登錄 https://console.firebase.google.com/,創建新項目。
  2. 在項目設置中添加 Android 應用(輸入包名,如 com.example.app)。
  3. 下載 google-services.json,放入 Android 項目的 app/ 目錄。
2.1.2 集成 FCM SDK 并獲取 Token

build.gradle(Module: app)中添加依賴:

dependencies {implementation 'com.google.firebase:firebase-messaging:23.6.0'
}

通過 FirebaseMessagingService 監聽 Token 生成:

class MyFirebaseMessagingService : FirebaseMessagingService() {// Token 生成或刷新時回調override fun onNewToken(token: String) {super.onNewToken(token)// 上報 Token 到服務器(關聯用戶 ID)reportDeviceTokenToServer(token)}private fun reportDeviceTokenToServer(token: String) {val userId = getCurrentUserId() // 用戶登錄后獲取val retrofit = Retrofit.Builder().baseUrl("https://your-server.com/").addConverterFactory(GsonConverterFactory.create()).build()val service = retrofit.create(DeviceTokenApi::class.java)service.registerDeviceToken(userId, token).enqueue(object : Callback<Void> {override fun onResponse(call: Call<Void>, response: Response<Void>) {Log.d("FCM", "Token 上報成功")}override fun onFailure(call: Call<Void>, t: Throwable) {Log.e("FCM", "Token 上報失敗: ${t.message}")// 本地緩存 Token,后續重試}})}
}// 設備 Token 上報接口
interface DeviceTokenApi {@POST("device-token/register")fun registerDeviceToken(@Query("userId") userId: String,@Query("token") token: String): Call<Void>
}

2.2 iOS:集成 APNs 獲取 Token

APNs(Apple Push Notification service)是 iOS 官方推送服務,需通過證書認證。

2.2.1 生成 APNs 證書
  1. 登錄 https://developer.apple.com/account/,創建“推送通知”證書(開發/生產環境)。
  2. 導出 .p12 證書(用于服務器端簽名推送請求)。
2.2.2 配置 Xcode 項目
  1. 在 Xcode 中啟用“Push Notifications”能力,確保 Bundle ID 與開發者后臺一致。
  2. AppDelegate 中監聽 Token 生成:
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {UNUserNotificationCenter.current().delegate = selfapplication.registerForRemoteNotifications()return true}// 獲取設備 Token 成功func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()reportDeviceTokenToServer(token)}private func reportDeviceTokenToServer(_ token: String) {let userId = getCurrentUserId() // 用戶登錄后獲取let url = URL(string: "https://your-server.com/device-token/register")!var request = URLRequest(url: url)request.httpMethod = "POST"request.httpBody = try? JSONEncoder().encode(["userId": userId, "token": token])URLSession.shared.dataTask(with: request) { _, _, error inif let error = error {print("Token 上報失敗: \(error.localizedDescription)")// 本地緩存 Token,后續重試} else {print("Token 上報成功")}}.resume()}
}

三、服務端開發:推送觸發與消息存儲

3.1 系統推送服務集成

服務器需通過 FCM 向 Android 設備推送,通過 APNs 向 iOS 設備推送。

3.1.1 FCM 推送實現(Android)

使用 Firebase Admin SDK 發送推送:

@Component
public class FcmPushClient {private final FirebaseApp firebaseApp;public FcmPushClient() throws IOException {// 加載 google-services.jsonFirebaseOptions options = new FirebaseOptions.Builder().setCredentials(GoogleCredentials.fromStream(getClass().getResourceAsStream("/google-services.json"))).build();FirebaseApp.initializeApp(options);this.firebaseApp = FirebaseApp.getInstance();}public void sendFcmNotification(String deviceToken, NotificationMessage message) {FirebaseMessaging messaging = FirebaseMessaging.getInstance(firebaseApp);Message fcmMessage = Message.builder().setToken(deviceToken).putAllData(buildFcmData(message)).build();try {String response = messaging.send(fcmMessage);log.info("FCM 推送成功,響應: {}", response);} catch (FirebaseMessagingException e) {log.error("FCM 推送失敗: {}", e.getMessage());// 記錄失敗 Token,后續清理}}private Map<String, Object> buildFcmData(NotificationMessage message) {Map<String, Object> data = new HashMap<>();data.put("title", message.getTitle());   // 通知標題data.put("body", message.getContent());  // 通知內容data.put("click_action", "OPEN_ORDER_DETAIL"); // 點擊 Actiondata.put("orderId", message.getOrderId());     // 跳轉參數return data;}
}
3.1.2 APNs 推送實現(iOS)

使用 pushy 庫發送 APNs 推送:

@Component
public class ApnsPushClient {private final ApnsClient apnsClient;public ApnsPushClient(@Value("${apns.cert-path}") String certPath,@Value("${apns.team-id}") String teamId,@Value("${apns.key-id}") String keyId,@Value("${apns.bundle-id}") String bundleId) throws Exception {ApnsSigningKey signingKey = ApnsSigningKey.loadFromPkcs8File(new File(certPath), teamId, keyId);this.apnsClient = new ApnsClientBuilder().setSigningKey(signingKey).setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST).build();}public void sendApnsNotification(String deviceToken, NotificationMessage message) {ApnsNotification apnsNotification = new ApnsNotification(deviceToken,new ApnsPayloadBuilder().setAlertTitle(message.getTitle()).setAlertBody(message.getContent()).setSound("default").setBadge(1).build());apnsNotification.getCustomData().put("orderId", message.getOrderId());try {Future<PushNotificationResponse<ApnsNotification>> future = apnsClient.sendNotification(apnsNotification);PushNotificationResponse<ApnsNotification> response = future.get();if (!response.isAccepted()) {log.error("APNs 推送失敗: {}", response.getRejectionReason());}} catch (Exception e) {log.error("APNs 推送異常: {}", e.getMessage());}}
}

3.2 消息存儲:Redis 持久化未讀通知

使用 Redis 存儲未讀通知,確保用戶打開 App 后能拉取所有未讀消息。

3.2.1 消息實體設計
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AppNotification {private String notificationId; // 全局唯一 ID(UUID)private String userId;         // 用戶 IDprivate String title;          // 通知標題(通知欄顯示)private String content;        // 通知內容(通知欄顯示)private String jumpUrl;        // 點擊跳轉鏈接(如訂單詳情頁)private long timestamp;        // 時間戳(毫秒級)private int priority;          // 優先級(1-高,2-普通)private boolean isRead;        // 是否已讀(默認 false)
}
3.2.2 Redis 存儲服務實現
@Service
@RequiredArgsConstructor
public class NotificationStorageService {private final RedisTemplate<String, AppNotification> redisTemplate;private final ObjectMapper objectMapper;// 存儲未讀通知到 Redis ZSET(按時間排序)public void saveUnreadNotification(AppNotification notification) {String key = "app_notifications:" + notification.getUserId();try {String value = objectMapper.writeValueAsString(notification);redisTemplate.opsForZSet().add(key, value, notification.getTimestamp());redisTemplate.expire(key, 7, TimeUnit.DAYS); // 7 天過期} catch (JsonProcessingException e) {log.error("通知序列化失敗: {}", e.getMessage());throw new RuntimeException("通知存儲失敗");}}// 拉取未讀通知(最近 20 條,按時間倒序)public List<AppNotification> fetchUnreadNotifications(String userId) {String key = "app_notifications:" + userId;Set<String> notifications = redisTemplate.opsForZSet().range(key, 0, 19);return notifications.stream().map(this::deserializeNotification).collect(Collectors.toList());}private AppNotification deserializeNotification(String json) {try {return objectMapper.readValue(json, AppNotification.class);} catch (JsonProcessingException e) {log.error("通知反序列化失敗: {}", e.getMessage());return null;}}
}

3.3 推送觸發邏輯:在線/離線判斷

服務器需判斷 App 是否在線(通過心跳或 SSE 連接狀態),決定是否觸發系統推送。

@Service
@RequiredArgsConstructor
public class NotificationService {private final FcmPushClient fcmPushClient;private final ApnsPushClient apnsPushClient;private final NotificationStorageService storageService;private final DeviceTokenService deviceTokenService;private final RedisTemplate<String, String> redisTemplate;public void sendNotification(NotificationMessage message) {String userId = message.getUserId();String deviceToken = deviceTokenService.getDeviceToken(userId);if (deviceToken == null) {log.warn("用戶 {} 無有效設備 Token,無法推送", userId);return;}AppNotification notification = AppNotification.builder().notificationId(UUID.randomUUID().toString()).userId(userId).title(message.getTitle()).content(message.getContent()).jumpUrl(message.getJumpUrl()).timestamp(System.currentTimeMillis()).priority(message.getPriority()).isRead(false).build();// 判斷 App 是否在線(通過 Redis 心跳記錄)boolean isAppOnline = redisTemplate.hasKey("app_heartbeat:" + userId);if (isAppOnline) {// 在線:通過 SSE 實時推送(略)sseService.pushToUser(userId, notification);} else {// 離線:觸發系統推送 + 存儲if (deviceToken.startsWith("fcm_")) {fcmPushClient.sendFcmNotification(deviceToken, message);} else if (deviceToken.startsWith("apns_")) {apnsPushClient.sendApnsNotification(deviceToken, message);}storageService.saveUnreadNotification(notification);}}
}

四、用戶喚醒與跳轉實現

用戶點擊通知后,App 需喚醒并跳轉至指定頁面(如訂單詳情頁)。

4.1 Android:通知點擊跳轉

通過 PendingIntent 配置跳轉目標:

object NotificationUtils {fun showNotification(context: Context, notification: AppNotification) {createNotificationChannel(context)val intent = Intent(context, OrderDetailActivity::class.java).apply {putExtra("orderId", extractOrderIdFromJumpUrl(notification.jumpUrl))flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK}val pendingIntent = PendingIntent.getActivity(context,0,intent,PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)val notificationBuilder = NotificationCompat.Builder(context, "order_channel").setContentTitle(notification.title).setContentText(notification.content).setSmallIcon(R.drawable.ic_order_notification).setContentIntent(pendingIntent).setAutoCancel(true).build()val manager = context.getSystemService(NotificationManager::class.java)manager.notify(notification.notificationId.hashCode(), notificationBuilder)}private fun extractOrderIdFromJumpUrl(jumpUrl: String): String {val pattern = Pattern.compile("orderId=(\\w+)")val matcher = pattern.matcher(jumpUrl)return if (matcher.find()) matcher.group(1) else ""}
}

4.2 iOS:通知點擊跳轉

通過 UNUserNotificationCenterDelegate 處理點擊事件:

extension AppDelegate: UNUserNotificationCenterDelegate {func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {let userInfo = response.notification.request.content.userInfoif let jumpUrl = userInfo["jumpUrl"] as? String {if let url = URL(string: jumpUrl) {let components = URLComponents(url: url, resolvingAgainstBaseURL: true)if let orderId = components?.queryItems?.first(where: { $0.name == "orderId" })?.value {let orderDetailVC = OrderDetailViewController()orderDetailVC.orderId = orderIdif let rootVC = window?.rootViewController {rootVC.navigationController?.pushViewController(orderDetailVC, animated: true)}}}}completionHandler()}
}

五、關鍵優化與注意事項

5.1 推送可靠性保障

  • Token 校驗:定期清理無效 Token(如用戶卸載 App 后,FCM/APNs 會返回 InvalidToken 錯誤)。
  • 重試機制:推送失敗時自動重試 3 次(使用 Spring Retry 注解)。
  • 持久化存儲:Redis 開啟 RDB+AOF 持久化,防止服務端宕機導致消息丟失。

5.2 用戶體驗優化

  • 通知優先級:高優先級通知(如支付成功)設置 priority: 1,確保立即顯示;普通通知(如系統公告)設置 priority: 2
  • 去重邏輯:為每條通知生成全局唯一 ID(UUID),客戶端記錄已讀 ID,避免重復展示。
  • 過期策略:設置 Redis 過期時間(如 7 天),自動清理長期未讀的舊消息。

5.3 多平臺適配

平臺系統推送服務設備 Token 格式通知點擊跳轉實現
AndroidFCMfcm_ 開頭的字符串配置 PendingIntent 跳轉目標 Activity
iOSAPNsapns_ 開頭的字符串監聽 UNUserNotificationCenter 事件

六、總結

通過 系統推送服務(APNs/FCM) 觸發手機通知欄提醒,結合 Redis 消息存儲 確保消息持久化,最終實現了“App 未啟動時用戶仍能感知通知”的目標。核心流程如下:

  1. 客戶端集成:Android 集成 FCM,iOS 集成 APNs,獲取設備 Token 并上報服務器。
  2. 服務器推送:判斷 App 離線時,通過系統推送發送通知,并存儲消息到 Redis。
  3. 用戶喚醒:用戶點擊通知后,App 被喚醒并跳轉至指定頁面,完成通知閉環。

該方案兼顧實時性與可靠性,適用于外賣、網約車、即時通訊等需要離線通知的場景。實際開發中可根據業務需求擴展功能(如多設備支持、短信補發),進一步提升用戶體驗。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/92113.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/92113.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/92113.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

WebView 中控制光標

在 WebView 中控制光標&#xff08;如移動焦點、獲取/設置光標位置、顯示/隱藏光標等&#xff09;需要根據具體場景和平臺&#xff08;Android/iOS/Web&#xff09;采用不同的方法。以下是常見場景的解決方案&#xff1a;一、Web 頁面中的光標控制&#xff08;JavaScript&#…

2025國賽數學建模C題詳細思路模型代碼獲取,備戰國賽算法解析——決策樹

2025國賽數學建模C題詳細思路模型代碼獲取見文末名片 決策樹算法&#xff1a;從原理到實戰&#xff08;數模小白友好版&#xff09; 1. 決策樹是什么&#xff1f;——用生活例子理解核心概念 想象你周末想決定是否去野餐&#xff0c;可能會這樣思考&#xff1a; 根節點&#xf…

從底層架構到多元場景:計算機構成與應用的深度剖析

一、引言1.1 研究背景與意義在當今數字化時代&#xff0c;計算機已成為推動社會進步和經濟發展的核心力量&#xff0c;其身影遍布生活、工作、學習的各個角落。從個人日常使用的筆記本電腦、智能手機&#xff0c;到企業運營中不可或缺的服務器、大型機&#xff0c;再到科研領域…

控制建模matlab練習08:根軌跡

此練習主要是&#xff1a;在matlab中繪制根軌跡的方法。 一、在matlab中建立對應系統 1、例如&#xff0c;對于如圖的反饋系統。 2、其中開環傳遞函數G(s)、閉環傳遞函數Gcl(s)。3、因此&#xff0c;其閉環傳遞函數的根軌跡&#xff0c;就可以直接在matlab中繪制出來。 4、直接…

【Spring Boot 快速入門】七、阿里云 OSS 文件上傳

這里寫自定義目錄標題準備阿里云 OSS參照官方 SDK 編寫入門程序案例數據準備案例集成阿里云 OSS前端測試代碼app.jsstyle.cssindex.html效果圖準備阿里云 OSS 注冊登錄阿里云&#xff0c;然后點擊控制臺&#xff0c;在左上角菜單欄搜索對象存儲 OSS&#xff0c;點擊并開通點擊…

分布式微服務--Nacos作為配置中心(二)

前言&#xff1a;Nacos 是什么&#xff1f; Nacos&#xff08;Naming and Configuration Service&#xff09;是阿里巴巴開源的一個更易于構建云原生應用的動態服務發現、配置管理和服務管理平臺。我們可以使用它&#xff1a; ?作為注冊中心&#xff08;服務發現&#xff09; …

家庭/公司內部網絡內網穿透:無公網IP怎么設置外網遠程訪問?

家庭寬帶內網穿透如何實現&#xff1f;需公網IP嗎&#xff1f;公司內部的網址服務怎么提供互聯網訪問&#xff1f;相信很多人都有遇到家庭網和公司內部網下&#xff0c;搭建了服務器&#xff0c;或網絡硬件設備&#xff0c;需要在異地遠程訪問使用的情況。家庭和公司內部寬帶內…

水庫防洪安全雨水情監測預警系統

水庫防洪安全雨水情監測預警系統是一種高度集成現代信息技術與水利工程管理的綜合性智能化管理平臺&#xff0c;該系統主要應用于水庫及其周邊流域的實時水情監測與預警工作。通過部署先進的傳感設備和監測網絡&#xff0c;該系統能夠全天候不間斷地采集水庫庫區及周邊區域的降…

【論文閱讀】Editing Large Language Models: Problems, Methods, and Opportunities

Editing Large Language Models: Problems, Methods, and Opportunities原文摘要研究背景與問題提出核心問題&#xff1a;盡管LLM已具備強大的能力&#xff0c;但如何長期維持其時效性并修正錯誤仍缺乏系統方法論。現狀&#xff1a;近年來&#xff0c;針對LLMs的模型編輯技術興…

金融數據可視化的強力引擎 —— QtitanDataGrid在金融行業的應用實踐

QtitanDataGrid是一款適用于Qt的商業化DataGrid 組件&#xff0c;它使得表格數據可以直接面向終端用戶。這個組件吸收了用戶界面結構顯示表格方面所有的現代化技術的精華&#xff0c;是目前Qt市場上唯一一款擁有如此高級功能和出色性能的網格組件。這個Qt數據網格組件使用純C創…

玩轉 InfluxDB 3:用 HTTP API 快速創建高效數據表

前言 說起時間序列數據庫,InfluxDB 絕對是業界響當當的明星。數據源源不斷涌入,能否高效存儲和查詢,直接決定你的業務能不能飛速跑起來。可你還在用客戶端或者命令行一點一點手動操作?朋友,這操作太老土,分分鐘拖慢節奏。 現在是 API 自動化時代,HTTP API 可幫你輕松搞…

stc32g利用硬件I2C配合中斷實現高效率異步無阻塞讀寫方法

I2C讀寫巨慢, 即使在400kbit/s下, 讀寫一個字節數據也要花費20多us, 這太慢了, 每讀寫一次設備的寄存器數據, 還要設備地址和寄存器地址, 又加了兩個字節數據, 我就讀了個傳感器的兩個字節數據而已, 動輒還要花費100us的阻塞時間, 這太浪費資源了針對這個問題, 我利用硬件I2C及…

生成式 AI 重塑自動駕駛仿真:4D 場景生成技術的突破與實踐

近年來&#xff0c;伴隨自動駕駛技術的快速發展&#xff0c;行業對于仿真測試平臺的精度、覆蓋率和可擴展性提出了更高要求。尤其在數據閉環迭代、長尾場景驗證及安全冗余驗證等關鍵環節中&#xff0c;高保真、高復雜度的場景生成能力正在成為測試體系的核心支撐。 傳統場景生…

Java 啟動命令的完整解析

以下為您提供的 Java 啟動命令的完整解析和優化建議: nohup java -server \ -XX:+PrintGCDateStamps \ -XX:+PrintGCTimeStamps \ -Xlogger:/home/logs/gc_`date +%Y%m%d_%H%M`.log \ -jar ytr.jar > /dev/null & 一、命令逐行解析 命令部分 功能說明 技術原理 nohup …

JVM中的垃圾回收暫停是什么,為什么會出現暫停,不同的垃圾回收機制暫停對比

JVM中的垃圾回收暫停是什么&#xff1f; 在Java虛擬機&#xff08;JVM&#xff09;中&#xff0c;垃圾回收暫停&#xff08;Garbage Collection Pause&#xff09;&#xff0c;也稱為“Stop-The-World”事件&#xff0c;是指當垃圾收集器執行特定階段時&#xff0c;所有應用程序…

Spearman 相關系數與 Pearson 相關系數的區別

核心區別對比表特征Pearson 相關系數Spearman 相關系數相關性類型線性相關單調相關計算基礎原始數據值數據排名&#xff08;秩&#xff09;公式數據要求連續變量&#xff0c;近似正態分布有序數據或連續變量異常值敏感性高度敏感不敏感取值范圍[-1, 1][-1, 1]單調關系檢測僅檢測…

sqli-labs靶場less36-less40

less361.我們打開靶場之后打開來看一下&#xff0c;輸入的內容會被轉義&#xff0c;依舊是寬字節注入2.使用以下寬字節注入&#xff0c;使用的是%df?id-1%df%27%20union%20select%201,database(),3--3.剩余內容與前面關卡基本一樣&#xff0c;只要使用上面的方法合成寬字節即可…

企業級 TinyMCE Vue 編輯器解決方案 – 配置優化與性能提升指南、自定義插件

## 簡介TinyMCE Vue 是官方提供的 TinyMCE 富文本編輯器的 Vue 組件封裝&#xff0c;支持 Vue 2 和 Vue 3。它讓你可以在 Vue 項目中快速集成強大的富文本編輯能力&#xff0c;支持多種插件、主題和自定義擴展&#xff0c;適用于博客、內容管理、后臺系統等多種場景。主要特性&…

【模電筆記】—— 直流穩壓電源——穩壓電路

Tips&#xff1a;本章節筆記建議讀者綜合學習&#xff0c;內容較多&#xff0c;可謂是模電相當重要的部分&#xff0c;因此部分知識點沒有做到詳細解釋。 1.穩壓電路的性能指標 &#xff08;同上節直流穩壓電源的主要技術指標【模電筆記】—— 直流穩壓電源——整流、濾波電路…

C++——設計模式

文章目錄一、面向對象的優點和缺點1.1 回答重點1.2 擴展知識二、面向對象的三大特點2.1 回答重點2.2 擴展知識三、設計模式的六大原則3.1 回答重點3.1.1 單一職責原則&#xff08;Single Responsibility Principle, SRP&#xff09;3.1.2 開放 - 封閉原則&#xff08;Open-Clos…