From 1e6eb9f248d37a880713dd4e41e33a2f6ff180ee Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期四, 26 三月 2026 18:12:35 +0800
Subject: [PATCH] feat:系统与安全:系统设置、APP版本设置、推送配置、Banner配置、健康科普、活动通知

---
 backend/sql/init.sql                                                                              |  145 +
 backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java  |   55 
 backend/src/main/java/com/tcm/training/admin/system/dto/ActivityNoticeCreateRequest.java          |   21 
 tcm-training-admin/src/pages/system/message-push-settings/style.less                              |   72 
 tcm-training-admin/src/pages/system/activity-notice/service.ts                                    |   21 
 backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionDetailVO.java                    |   24 
 tcm-training-admin/src/pages/system/banner-settings/service.ts                                    |   74 
 tcm-training-admin/src/pages/system/message-push-settings/index.tsx                               |  151 +
 backend/src/main/java/com/tcm/training/admin/system/vo/ActivityNoticeListItemVO.java              |   17 
 backend/src/main/java/com/tcm/training/admin/upload/UploadController.java                         |   69 
 backend/sql/update_activity_notice_schema.sql                                                     |   24 
 backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerBatchDeleteRequest.java       |   18 
 backend/src/main/java/com/tcm/training/admin/system/mapper/SystemSettingsMapper.java              |    9 
 backend/sql/update_message_push_settings_schema.sql                                               |   35 
 backend/src/test/java/com/tcm/training/admin/system/service/ActivityNoticeAppServiceTest.java     |   84 
 tcm-training-admin/src/pages/system/activity-notice/index.tsx                                     |  287 +++
 tcm-training-admin/src/pages/system/banner-settings/style.less                                    |  254 ++
 backend/src/main/java/com/tcm/training/admin/system/controller/SystemSettingsController.java      |   36 
 tcm-training-admin/src/pages/system/message-push-settings/service.ts                              |   14 
 backend/src/main/java/com/tcm/training/admin/system/entity/SystemBannerConfig.java                |   74 
 backend/src/main/java/com/tcm/training/admin/system/service/ActivityNoticeAppService.java         |  116 +
 backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java              |   42 
 backend/src/main/java/com/tcm/training/admin/system/service/SystemBannerAppService.java           |  313 +++
 backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerDetailVO.java                  |   27 
 tcm-training-admin/src/pages/system/basic-settings/service.ts                                     |   14 
 backend/sql/upgrade_rbac.sql                                                                      |  138 +
 tcm-training-admin/src/pages/system/app-version-settings/style.less                               |  153 +
 backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerSaveRequest.java              |   45 
 backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerPublishStatusRequest.java     |   16 
 backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerCourseOptionVO.java            |   19 
 tcm-training-admin/src/pages/system/app-version-settings/index.tsx                                |  392 ++++
 backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerPublishStatusVO.java           |   19 
 backend/src/main/java/com/tcm/training/admin/system/mapper/MessagePushConfigMapper.java           |    9 
 tcm-training-admin/src/pages/system/activity-notice/style.less                                    |  123 +
 backend/src/main/java/com/tcm/training/admin/system/vo/SystemSettingsVO.java                      |   17 
 backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java     |  154 +
 backend/src/main/java/com/tcm/training/admin/system/mapper/SystemBannerConfigMapper.java          |    9 
 backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionListItemVO.java                  |   18 
 backend/src/main/java/com/tcm/training/admin/system/entity/ActivityNotice.java                    |   44 
 backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingItemSaveRequest.java    |   25 
 backend/src/main/java/com/tcm/training/admin/system/controller/ActivityNoticeController.java      |   49 
 backend/src/main/java/com/tcm/training/admin/system/entity/MessagePushConfig.java                 |   50 
 backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerListItemVO.java                |   24 
 backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionCheckVO.java                     |   18 
 tcm-training-admin/src/pages/system/basic-settings/style.less                                     |  129 +
 backend/sql/update_system_settings_schema.sql                                                     |   29 
 backend/src/main/java/com/tcm/training/admin/system/dto/SystemSettingsSaveRequest.java            |   32 
 tcm-training-admin/src/pages/system/banner-settings/index.tsx                                     |  711 +++++++
 backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java                       |   34 
 backend/src/main/java/com/tcm/training/admin/system/entity/AppVersionConfig.java                  |   65 
 tcm-training-admin/src/pages/system/basic-settings/index.tsx                                      |  201 ++
 tcm-training-admin/src/pages/system/settings-redirect/index.tsx                                   |   34 
 tcm-training-admin/src/access.js                                                                  |    5 
 backend/src/main/java/com/tcm/training/admin/system/controller/MessagePushSettingsController.java |   37 
 backend/sql/update_app_version_settings_schema.sql                                                |   37 
 backend/src/main/java/com/tcm/training/admin/system/mapper/ActivityNoticeMapper.java              |    9 
 tcm-training-admin/config/routes.ts                                                               |   33 
 backend/src/main/java/com/tcm/training/admin/system/controller/SystemBannerController.java        |   91 +
 backend/src/main/java/com/tcm/training/admin/system/service/SystemSettingsAppService.java         |   64 
 backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java |  105 +
 backend/src/main/java/com/tcm/training/admin/system/entity/SystemSettings.java                    |   50 
 backend/sql/update_system_banner_schema.sql                                                       |   43 
 backend/src/main/java/com/tcm/training/admin/system/mapper/AppVersionConfigMapper.java            |    9 
 backend/pom.xml                                                                                   |    5 
 tcm-training-admin/src/pages/system/app-version-settings/service.ts                               |   31 
 backend/src/main/java/com/tcm/training/admin/system/vo/MessagePushSettingVO.java                  |   20 
 backend/src/main/java/com/tcm/training/admin/system/service/MessagePushSettingsAppService.java    |  190 ++
 backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingsSaveRequest.java       |   19 
 backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java                         |    7 
 69 files changed, 5,298 insertions(+), 10 deletions(-)

diff --git a/backend/pom.xml b/backend/pom.xml
index 5603236..8f946f6 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -105,6 +105,11 @@
             <version>3.25.10</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>io.github.jpush</groupId>
+            <artifactId>jiguang-sdk</artifactId>
+            <version>5.3.0</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/backend/sql/init.sql b/backend/sql/init.sql
index bd5d041..da38d93 100644
--- a/backend/sql/init.sql
+++ b/backend/sql/init.sql
@@ -101,6 +101,23 @@
 (1203, 1200, '编辑角色', 'system_role_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:role:edit', '角色编辑按钮', 0),
 (1204, 1200, '删除角色', 'system_role_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:role:delete', '角色删除按钮', 0),
 (1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1305, 1300, '系统设置', 'system_basic_settings', 'C', '/system/settings/basic', 'System/BasicSettings', NULL, 5, 1, 1, 'system:basic-settings:view', '系统设置菜单', 0),
+(1306, 1305, '编辑系统设置', 'system_basic_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:basic-settings:edit', '系统设置编辑按钮', 0),
+(1307, 1300, 'APP版本设置', 'system_app_version_settings', 'C', '/system/settings/app-version', 'System/AppVersionSettings', NULL, 15, 1, 1, 'system:app-version:view', 'APP版本设置菜单', 0),
+(1308, 1307, '新增APP版本', 'system_app_version_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:app-version:add', '新增APP版本按钮', 0),
+(1309, 1307, '查看APP版本', 'system_app_version_settings_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:app-version:detail', '查看APP版本按钮', 0),
+(1312, 1300, '消息推送配置', 'system_message_push_settings', 'C', '/system/settings/message-push', 'System/MessagePushSettings', NULL, 20, 1, 1, 'system:message-push:view', '消息推送配置菜单', 0),
+(1313, 1312, '编辑消息推送配置', 'system_message_push_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:message-push:edit', '消息推送配置编辑按钮', 0),
+(1314, 1300, 'Banner配置', 'system_banner_settings', 'C', '/system/settings/banner', 'System/BannerSettings', NULL, 12, 1, 1, 'system:banner:view', 'Banner配置菜单', 0),
+(1315, 1314, '新增Banner', 'system_banner_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:banner:add', 'Banner新增按钮', 0),
+(1316, 1314, '编辑Banner', 'system_banner_settings_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:banner:edit', 'Banner编辑按钮', 0),
+(1317, 1314, '查看Banner', 'system_banner_settings_detail', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:banner:detail', 'Banner查看按钮', 0),
+(1318, 1314, '删除Banner', 'system_banner_settings_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:banner:delete', 'Banner删除按钮', 0),
+(1319, 1314, '批量删除Banner', 'system_banner_settings_batch_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:banner:batch-delete', 'Banner批量删除按钮', 0),
+(1320, 1314, '切换Banner上架状态', 'system_banner_settings_publish_status', 'B', NULL, NULL, NULL, 6, 1, 1, 'system:banner:publish-status', 'Banner上架状态按钮', 0),
+(1321, 1300, '活动通知', 'system_activity_notice', 'C', '/system/settings/activity-notice', 'System/ActivityNotice', NULL, 13, 1, 1, 'system:activity-notice:view', '活动通知菜单', 0),
+(1322, 1321, '新增活动通知', 'system_activity_notice_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:activity-notice:add', '活动通知新增按钮', 0),
+(1323, 1321, '删除活动通知', 'system_activity_notice_delete', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:activity-notice:delete', '活动通知删除按钮', 0),
 (1310, 1300, '积分设置', 'system_points_settings', 'C', '/system/settings/points', 'System/PointsSettings', NULL, 10, 1, 1, 'system:points-settings:view', '积分设置菜单', 0),
 (1311, 1310, '编辑积分设置', 'system_points_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:points-settings:edit', '积分设置编辑按钮', 0),
 (2000, 0, '用户与会员', 'member_root', 'M', '/member', NULL, 'team', 200, 1, 1, NULL, '用户与会员模块', 0),
@@ -186,7 +203,7 @@
 INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
 VALUES
 (1, 1000), (1, 1100), (1, 1101), (1, 1102), (1, 1103), (1, 1104), (1, 1105),
-(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204), (1, 1300), (1, 1310), (1, 1311),
+(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204), (1, 1300), (1, 1305), (1, 1306), (1, 1307), (1, 1308), (1, 1309), (1, 1310), (1, 1311), (1, 1312), (1, 1313), (1, 1314), (1, 1315), (1, 1316), (1, 1317), (1, 1318), (1, 1319), (1, 1320), (1, 1321), (1, 1322), (1, 1323),
 (1, 2000), (1, 2100), (1, 2110), (1, 2111), (1, 2112), (1, 2113), (1, 2114), (1, 2115), (1, 2116), (1, 2117), (1, 2118), (1, 2119),
 (1, 2200), (1, 2210), (1, 2211), (1, 2220), (1, 2221),
 (1, 3000), (1, 3050), (1, 3060), (1, 3061), (1, 3062), (1, 3063),
@@ -773,6 +790,132 @@
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='场次答题记录表';
 
 -- ----------------------------
+-- 系统设置
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_system_settings` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `service_phone` VARCHAR(32) NOT NULL COMMENT '客服电话',
+  `service_online_time` VARCHAR(64) NOT NULL COMMENT '客服在线时间',
+  `backup_cycle_days` INT NOT NULL COMMENT '备份周期(天)',
+  `member_expire_remind_days` INT NOT NULL COMMENT '会员到期提醒天数',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';
+
+INSERT IGNORE INTO `tcm_system_settings`
+(`id`, `service_phone`, `service_online_time`, `backup_cycle_days`, `member_expire_remind_days`, `deleted`)
+VALUES
+(1, '400-800-1234', '09:00-18:00', 7, 3, 0);
+
+-- ----------------------------
+-- APP版本设置
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_app_version_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
+  `version_code` INT NOT NULL COMMENT '版本字段',
+  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
+  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
+  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
+  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
+  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
+  `update_notice` TEXT NOT NULL COMMENT '更新说明',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_app_version_code` (`version_code`),
+  KEY `idx_app_version_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP版本设置表';
+
+INSERT IGNORE INTO `tcm_app_version_config`
+(`id`, `version_no`, `version_code`, `android_file_url`, `android_file_name`, `ios_file_url`, `ios_file_name`, `force_update`, `update_notice`, `deleted`)
+VALUES
+(1, '1.0.0', 1, 'https://example.com/app/android/1.0.0.apk', 'app-1.0.0.apk', 'https://example.com/app/ios/1.0.0.ipa', 'app-1.0.0.ipa', 0, '反馈内容反馈内容反馈内容反馈内容反馈内容', 0),
+(2, '1.0.1', 2, 'https://example.com/app/android/1.0.1.apk', 'app-1.0.1.apk', 'https://example.com/app/ios/1.0.1.ipa', 'app-1.0.1.ipa', 1, '反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容', 0);
+
+-- ----------------------------
+-- 消息推送配置
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_message_push_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `notice_type_code` VARCHAR(64) NOT NULL COMMENT '通知类型编码',
+  `notice_type_name` VARCHAR(64) NOT NULL COMMENT '通知类型名称',
+  `push_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启PUSH推送:1是 0否',
+  `sms_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启短信通知:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_message_push_notice_type_code` (`notice_type_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息推送配置表';
+
+INSERT IGNORE INTO `tcm_message_push_config`
+(`id`, `notice_type_code`, `notice_type_name`, `push_enabled`, `sms_enabled`, `deleted`)
+VALUES
+(1, 'MEMBER_EXPIRE_REMIND', '会员到期提醒', 1, 1, 0),
+(2, 'LIVE_REMIND', '直播提醒', 1, 1, 0),
+(3, 'ACTIVITY_NOTICE', '活动通知', 1, 0, 0),
+(4, 'ORDER_STATUS_UPDATE', '订单状态更新', 1, 0, 0),
+(5, 'COURSE_UPDATE_NOTICE', '课程更新通知', 1, 0, 0),
+(6, 'STUDY_PROGRESS_REMIND', '学习进度提醒', 1, 0, 0);
+
+-- ----------------------------
+-- Banner配置
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_system_banner_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `banner_img` VARCHAR(512) NOT NULL COMMENT 'Banner图片',
+  `title` VARCHAR(64) NOT NULL COMMENT 'Banner标题',
+  `start_at` DATETIME NOT NULL COMMENT '展示开始时间',
+  `end_at` DATETIME NOT NULL COMMENT '展示结束时间',
+  `jump_type` VARCHAR(32) NOT NULL COMMENT '跳转类型',
+  `target_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '跳转目标ID',
+  `target_name` VARCHAR(64) DEFAULT NULL COMMENT '跳转目标名称',
+  `target_url` VARCHAR(512) DEFAULT NULL COMMENT '三方地址',
+  `publish_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否上架:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_system_banner_time` (`start_at`, `end_at`),
+  KEY `idx_system_banner_jump_type` (`jump_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Banner配置表';
+
+INSERT IGNORE INTO `tcm_system_banner_config`
+(`id`, `banner_img`, `title`, `start_at`, `end_at`, `jump_type`, `target_id`, `target_name`, `target_url`, `publish_enabled`, `deleted`)
+VALUES
+(1, 'https://example.com/banner/member-discount.png', '会员显示折扣', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'MEMBER', NULL, '会员', NULL, 1, 0),
+(2, 'https://example.com/banner/health-course.png', '兑换商城优惠券更新', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'HEALTH_COURSE', 1, '养生课程1', NULL, 1, 0),
+(3, 'https://example.com/banner/third-party.png', '标题标题题', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'THIRD_PARTY_URL', NULL, NULL, 'https://www.baidu.com', 0, 0);
+
+-- ----------------------------
+-- 活动通知
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_activity_notice` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `title` VARCHAR(15) NOT NULL COMMENT '通知标题',
+  `content_html` LONGTEXT NOT NULL COMMENT '通知内容富文本',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_activity_notice_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动通知表';
+
+-- ----------------------------
 -- 积分设置
 -- ----------------------------
 CREATE TABLE IF NOT EXISTS `tcm_points_config` (
diff --git a/backend/sql/update_activity_notice_schema.sql b/backend/sql/update_activity_notice_schema.sql
new file mode 100644
index 0000000..2ed896e
--- /dev/null
+++ b/backend/sql/update_activity_notice_schema.sql
@@ -0,0 +1,24 @@
+CREATE TABLE IF NOT EXISTS `tcm_activity_notice` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `title` VARCHAR(15) NOT NULL COMMENT '通知标题',
+  `content_html` LONGTEXT NOT NULL COMMENT '通知内容富文本',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_activity_notice_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动通知表';
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1321, 1300, '活动通知', 'system_activity_notice', 'C', '/system/settings/activity-notice', 'System/ActivityNotice', NULL, 13, 1, 1, 'system:activity-notice:view', '活动通知菜单', 0),
+(1322, 1321, '新增活动通知', 'system_activity_notice_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:activity-notice:add', '活动通知新增按钮', 0),
+(1323, 1321, '删除活动通知', 'system_activity_notice_delete', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:activity-notice:delete', '活动通知删除按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1321), (1, 1322), (1, 1323);
diff --git a/backend/sql/update_app_version_settings_schema.sql b/backend/sql/update_app_version_settings_schema.sql
new file mode 100644
index 0000000..bae465b
--- /dev/null
+++ b/backend/sql/update_app_version_settings_schema.sql
@@ -0,0 +1,37 @@
+CREATE TABLE IF NOT EXISTS `tcm_app_version_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
+  `version_code` INT NOT NULL COMMENT '版本字段',
+  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
+  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
+  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
+  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
+  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
+  `update_notice` TEXT NOT NULL COMMENT '更新说明',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_app_version_code` (`version_code`),
+  KEY `idx_app_version_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP版本设置表';
+
+INSERT IGNORE INTO `tcm_app_version_config`
+(`id`, `version_no`, `version_code`, `android_file_url`, `android_file_name`, `ios_file_url`, `ios_file_name`, `force_update`, `update_notice`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, '1.0.0', 1, 'https://example.com/app/android/1.0.0.apk', 'app-1.0.0.apk', 'https://example.com/app/ios/1.0.0.ipa', 'app-1.0.0.ipa', 0, '反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化'),
+(2, '1.0.1', 2, 'https://example.com/app/android/1.0.1.apk', 'app-1.0.1.apk', 'https://example.com/app/ios/1.0.1.ipa', 'app-1.0.1.ipa', 1, '反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化');
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1307, 1300, 'APP版本设置', 'system_app_version_settings', 'C', '/system/settings/app-version', 'System/AppVersionSettings', NULL, 15, 1, 1, 'system:app-version:view', 'APP版本设置菜单', 0),
+(1308, 1307, '新增APP版本', 'system_app_version_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:app-version:add', '新增APP版本按钮', 0),
+(1309, 1307, '查看APP版本', 'system_app_version_settings_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:app-version:detail', '查看APP版本按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1307), (1, 1308), (1, 1309);
diff --git a/backend/sql/update_message_push_settings_schema.sql b/backend/sql/update_message_push_settings_schema.sql
new file mode 100644
index 0000000..f4bcd5c
--- /dev/null
+++ b/backend/sql/update_message_push_settings_schema.sql
@@ -0,0 +1,35 @@
+CREATE TABLE IF NOT EXISTS `tcm_message_push_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `notice_type_code` VARCHAR(64) NOT NULL COMMENT '通知类型编码',
+  `notice_type_name` VARCHAR(64) NOT NULL COMMENT '通知类型名称',
+  `push_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启PUSH推送:1是 0否',
+  `sms_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启短信通知:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_message_push_notice_type_code` (`notice_type_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息推送配置表';
+
+INSERT IGNORE INTO `tcm_message_push_config`
+(`id`, `notice_type_code`, `notice_type_name`, `push_enabled`, `sms_enabled`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, 'MEMBER_EXPIRE_REMIND', '会员到期提醒', 1, 1, 0, '系统初始化', '系统初始化'),
+(2, 'LIVE_REMIND', '直播提醒', 1, 1, 0, '系统初始化', '系统初始化'),
+(3, 'ACTIVITY_NOTICE', '活动通知', 1, 0, 0, '系统初始化', '系统初始化'),
+(4, 'ORDER_STATUS_UPDATE', '订单状态更新', 1, 0, 0, '系统初始化', '系统初始化'),
+(5, 'COURSE_UPDATE_NOTICE', '课程更新通知', 1, 0, 0, '系统初始化', '系统初始化'),
+(6, 'STUDY_PROGRESS_REMIND', '学习进度提醒', 1, 0, 0, '系统初始化', '系统初始化');
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1312, 1300, '消息推送配置', 'system_message_push_settings', 'C', '/system/settings/message-push', 'System/MessagePushSettings', NULL, 20, 1, 1, 'system:message-push:view', '消息推送配置菜单', 0),
+(1313, 1312, '编辑消息推送配置', 'system_message_push_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:message-push:edit', '消息推送配置编辑按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1312), (1, 1313);
diff --git a/backend/sql/update_system_banner_schema.sql b/backend/sql/update_system_banner_schema.sql
new file mode 100644
index 0000000..6de2351
--- /dev/null
+++ b/backend/sql/update_system_banner_schema.sql
@@ -0,0 +1,43 @@
+CREATE TABLE IF NOT EXISTS `tcm_system_banner_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `banner_img` VARCHAR(512) NOT NULL COMMENT 'Banner图片',
+  `title` VARCHAR(64) NOT NULL COMMENT 'Banner标题',
+  `start_at` DATETIME NOT NULL COMMENT '展示开始时间',
+  `end_at` DATETIME NOT NULL COMMENT '展示结束时间',
+  `jump_type` VARCHAR(32) NOT NULL COMMENT '跳转类型',
+  `target_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '跳转目标ID',
+  `target_name` VARCHAR(64) DEFAULT NULL COMMENT '跳转目标名称',
+  `target_url` VARCHAR(512) DEFAULT NULL COMMENT '三方地址',
+  `publish_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否上架:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_system_banner_time` (`start_at`, `end_at`),
+  KEY `idx_system_banner_jump_type` (`jump_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Banner配置表';
+
+INSERT IGNORE INTO `tcm_system_banner_config`
+(`id`, `banner_img`, `title`, `start_at`, `end_at`, `jump_type`, `target_id`, `target_name`, `target_url`, `publish_enabled`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, 'https://example.com/banner/member-discount.png', '会员显示折扣', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'MEMBER', NULL, '会员', NULL, 1, 0, '系统初始化', '系统初始化'),
+(2, 'https://example.com/banner/health-course.png', '兑换商城优惠券更新', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'HEALTH_COURSE', 1, '养生课程1', NULL, 1, 0, '系统初始化', '系统初始化'),
+(3, 'https://example.com/banner/third-party.png', '标题标题题', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'THIRD_PARTY_URL', NULL, NULL, 'https://www.baidu.com', 0, 0, '系统初始化', '系统初始化');
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1314, 1300, 'Banner配置', 'system_banner_settings', 'C', '/system/settings/banner', 'System/BannerSettings', NULL, 12, 1, 1, 'system:banner:view', 'Banner配置菜单', 0),
+(1315, 1314, '新增Banner', 'system_banner_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:banner:add', 'Banner新增按钮', 0),
+(1316, 1314, '编辑Banner', 'system_banner_settings_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:banner:edit', 'Banner编辑按钮', 0),
+(1317, 1314, '查看Banner', 'system_banner_settings_detail', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:banner:detail', 'Banner查看按钮', 0),
+(1318, 1314, '删除Banner', 'system_banner_settings_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:banner:delete', 'Banner删除按钮', 0),
+(1319, 1314, '批量删除Banner', 'system_banner_settings_batch_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:banner:batch-delete', 'Banner批量删除按钮', 0),
+(1320, 1314, '切换Banner上架状态', 'system_banner_settings_publish_status', 'B', NULL, NULL, NULL, 6, 1, 1, 'system:banner:publish-status', 'Banner上架状态按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1314), (1, 1315), (1, 1316), (1, 1317), (1, 1318), (1, 1319), (1, 1320);
diff --git a/backend/sql/update_system_settings_schema.sql b/backend/sql/update_system_settings_schema.sql
new file mode 100644
index 0000000..5345e95
--- /dev/null
+++ b/backend/sql/update_system_settings_schema.sql
@@ -0,0 +1,29 @@
+CREATE TABLE IF NOT EXISTS `tcm_system_settings` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `service_phone` VARCHAR(32) NOT NULL COMMENT '客服电话',
+  `service_online_time` VARCHAR(64) NOT NULL COMMENT '客服在线时间',
+  `backup_cycle_days` INT NOT NULL COMMENT '备份周期(天)',
+  `member_expire_remind_days` INT NOT NULL COMMENT '会员到期提醒天数',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';
+
+INSERT IGNORE INTO `tcm_system_settings`
+(`id`, `service_phone`, `service_online_time`, `backup_cycle_days`, `member_expire_remind_days`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, '400-800-1234', '09:00-18:00', 7, 3, 0, '系统初始化', '系统初始化');
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1305, 1300, '系统设置', 'system_basic_settings', 'C', '/system/settings/basic', 'System/BasicSettings', NULL, 5, 1, 1, 'system:basic-settings:view', '系统设置菜单', 0),
+(1306, 1305, '编辑系统设置', 'system_basic_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:basic-settings:edit', '系统设置编辑按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1305), (1, 1306);
diff --git a/backend/sql/upgrade_rbac.sql b/backend/sql/upgrade_rbac.sql
index fdd8688..7c15b9b 100644
--- a/backend/sql/upgrade_rbac.sql
+++ b/backend/sql/upgrade_rbac.sql
@@ -238,6 +238,21 @@
 (1202, 1200, '新增角色', 'system_role_add', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:role:add', '角色新增按钮', 0),
 (1203, 1200, '编辑角色', 'system_role_edit', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:role:edit', '角色编辑按钮', 0),
 (1204, 1200, '删除角色', 'system_role_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:role:delete', '角色删除按钮', 0),
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1305, 1300, '系统设置', 'system_basic_settings', 'C', '/system/settings/basic', 'System/BasicSettings', NULL, 5, 1, 1, 'system:basic-settings:view', '系统设置菜单', 0),
+(1306, 1305, '编辑系统设置', 'system_basic_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:basic-settings:edit', '系统设置编辑按钮', 0),
+(1307, 1300, 'APP版本设置', 'system_app_version_settings', 'C', '/system/settings/app-version', 'System/AppVersionSettings', NULL, 15, 1, 1, 'system:app-version:view', 'APP版本设置菜单', 0),
+(1308, 1307, '新增APP版本', 'system_app_version_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:app-version:add', '新增APP版本按钮', 0),
+(1309, 1307, '查看APP版本', 'system_app_version_settings_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:app-version:detail', '查看APP版本按钮', 0),
+(1312, 1300, '消息推送配置', 'system_message_push_settings', 'C', '/system/settings/message-push', 'System/MessagePushSettings', NULL, 20, 1, 1, 'system:message-push:view', '消息推送配置菜单', 0),
+(1313, 1312, '编辑消息推送配置', 'system_message_push_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:message-push:edit', '消息推送配置编辑按钮', 0),
+(1314, 1300, 'Banner配置', 'system_banner_settings', 'C', '/system/settings/banner', 'System/BannerSettings', NULL, 12, 1, 1, 'system:banner:view', 'Banner配置菜单', 0),
+(1315, 1314, '新增Banner', 'system_banner_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:banner:add', 'Banner新增按钮', 0),
+(1316, 1314, '编辑Banner', 'system_banner_settings_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:banner:edit', 'Banner编辑按钮', 0),
+(1317, 1314, '查看Banner', 'system_banner_settings_detail', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:banner:detail', 'Banner查看按钮', 0),
+(1318, 1314, '删除Banner', 'system_banner_settings_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:banner:delete', 'Banner删除按钮', 0),
+(1319, 1314, '批量删除Banner', 'system_banner_settings_batch_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:banner:batch-delete', 'Banner批量删除按钮', 0),
+(1320, 1314, '切换Banner上架状态', 'system_banner_settings_publish_status', 'B', NULL, NULL, NULL, 6, 1, 1, 'system:banner:publish-status', 'Banner上架状态按钮', 0),
 (2000, 0, '用户与会员', 'member_root', 'M', '/member', NULL, 'team', 200, 1, 1, NULL, '用户与会员模块', 0),
 (2100, 2000, '用户', 'member_user_group', 'M', '/member/user', NULL, NULL, 10, 1, 1, NULL, '用户分组菜单', 0),
 (2110, 2100, '用户管理', 'member_user_manage', 'C', '/member/user/manage', 'Member/UserList', NULL, 10, 1, 1, 'member:user:view', '用户管理菜单', 0),
@@ -281,7 +296,7 @@
 INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
 VALUES
 (1, 1000), (1, 1100), (1, 1101), (1, 1102), (1, 1103), (1, 1104), (1, 1105),
-(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204),
+(1, 1200), (1, 1201), (1, 1202), (1, 1203), (1, 1204), (1, 1300), (1, 1305), (1, 1306), (1, 1307), (1, 1308), (1, 1309),
 (1, 2000), (1, 2100), (1, 2110), (1, 2111), (1, 2112), (1, 2113), (1, 2114), (1, 2115), (1, 2116), (1, 2117), (1, 2118), (1, 2119),
 (1, 2200), (1, 2210), (1, 2211), (1, 2220), (1, 2221),
 (1, 3000), (1, 3050), (1, 3060), (1, 3061), (1, 3062), (1, 3063),
@@ -713,3 +728,124 @@
 SELECT `id`, 1
 FROM `sys_user`
 WHERE `username` = 'admin';
+
+CREATE TABLE IF NOT EXISTS `tcm_system_settings` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `service_phone` VARCHAR(32) NOT NULL COMMENT '客服电话',
+  `service_online_time` VARCHAR(64) NOT NULL COMMENT '客服在线时间',
+  `backup_cycle_days` INT NOT NULL COMMENT '备份周期(天)',
+  `member_expire_remind_days` INT NOT NULL COMMENT '会员到期提醒天数',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统设置表';
+
+INSERT IGNORE INTO `tcm_system_settings`
+(`id`, `service_phone`, `service_online_time`, `backup_cycle_days`, `member_expire_remind_days`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, '400-800-1234', '09:00-18:00', 7, 3, 0, '系统初始化', '系统初始化');
+
+INSERT IGNORE INTO `sys_menu`
+(`id`, `parent_id`, `name`, `menu_code`, `type`, `path`, `component`, `icon`, `sort`, `status`, `visible`, `permission_key`, `remark`, `deleted`)
+VALUES
+(1300, 1000, '系统设置', 'system_setting_group', 'M', '/system/settings', NULL, NULL, 30, 1, 1, NULL, '系统设置目录', 0),
+(1305, 1300, '系统设置', 'system_basic_settings', 'C', '/system/settings/basic', 'System/BasicSettings', NULL, 5, 1, 1, 'system:basic-settings:view', '系统设置菜单', 0),
+(1306, 1305, '编辑系统设置', 'system_basic_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:basic-settings:edit', '系统设置编辑按钮', 0),
+(1307, 1300, 'APP版本设置', 'system_app_version_settings', 'C', '/system/settings/app-version', 'System/AppVersionSettings', NULL, 15, 1, 1, 'system:app-version:view', 'APP版本设置菜单', 0),
+(1308, 1307, '新增APP版本', 'system_app_version_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:app-version:add', '新增APP版本按钮', 0),
+(1309, 1307, '查看APP版本', 'system_app_version_settings_detail', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:app-version:detail', '查看APP版本按钮', 0),
+(1312, 1300, '消息推送配置', 'system_message_push_settings', 'C', '/system/settings/message-push', 'System/MessagePushSettings', NULL, 20, 1, 1, 'system:message-push:view', '消息推送配置菜单', 0),
+(1313, 1312, '编辑消息推送配置', 'system_message_push_settings_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:message-push:edit', '消息推送配置编辑按钮', 0),
+(1314, 1300, 'Banner配置', 'system_banner_settings', 'C', '/system/settings/banner', 'System/BannerSettings', NULL, 12, 1, 1, 'system:banner:view', 'Banner配置菜单', 0),
+(1315, 1314, '新增Banner', 'system_banner_settings_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'system:banner:add', 'Banner新增按钮', 0),
+(1316, 1314, '编辑Banner', 'system_banner_settings_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'system:banner:edit', 'Banner编辑按钮', 0),
+(1317, 1314, '查看Banner', 'system_banner_settings_detail', 'B', NULL, NULL, NULL, 3, 1, 1, 'system:banner:detail', 'Banner查看按钮', 0),
+(1318, 1314, '删除Banner', 'system_banner_settings_delete', 'B', NULL, NULL, NULL, 4, 1, 1, 'system:banner:delete', 'Banner删除按钮', 0),
+(1319, 1314, '批量删除Banner', 'system_banner_settings_batch_delete', 'B', NULL, NULL, NULL, 5, 1, 1, 'system:banner:batch-delete', 'Banner批量删除按钮', 0),
+(1320, 1314, '切换Banner上架状态', 'system_banner_settings_publish_status', 'B', NULL, NULL, NULL, 6, 1, 1, 'system:banner:publish-status', 'Banner上架状态按钮', 0);
+
+INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`)
+VALUES
+(1, 1300), (1, 1305), (1, 1306), (1, 1307), (1, 1308), (1, 1309), (1, 1312), (1, 1313), (1, 1314), (1, 1315), (1, 1316), (1, 1317), (1, 1318), (1, 1319), (1, 1320);
+
+CREATE TABLE IF NOT EXISTS `tcm_app_version_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `version_no` VARCHAR(32) NOT NULL COMMENT '展示版本号',
+  `version_code` INT NOT NULL COMMENT '版本字段',
+  `android_file_url` VARCHAR(512) NOT NULL COMMENT '安卓文件地址',
+  `android_file_name` VARCHAR(255) NOT NULL COMMENT '安卓文件名称',
+  `ios_file_url` VARCHAR(512) NOT NULL COMMENT 'iOS文件地址',
+  `ios_file_name` VARCHAR(255) NOT NULL COMMENT 'iOS文件名称',
+  `force_update` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否强制更新:1是 0否',
+  `update_notice` TEXT NOT NULL COMMENT '更新说明',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_app_version_code` (`version_code`),
+  KEY `idx_app_version_created_at` (`created_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP版本设置表';
+
+INSERT IGNORE INTO `tcm_app_version_config`
+(`id`, `version_no`, `version_code`, `android_file_url`, `android_file_name`, `ios_file_url`, `ios_file_name`, `force_update`, `update_notice`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, '1.0.0', 1, 'https://example.com/app/android/1.0.0.apk', 'app-1.0.0.apk', 'https://example.com/app/ios/1.0.0.ipa', 'app-1.0.0.ipa', 0, '反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化'),
+(2, '1.0.1', 2, 'https://example.com/app/android/1.0.1.apk', 'app-1.0.1.apk', 'https://example.com/app/ios/1.0.1.ipa', 'app-1.0.1.ipa', 1, '反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容反馈内容', 0, '系统初始化', '系统初始化');
+
+CREATE TABLE IF NOT EXISTS `tcm_message_push_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `notice_type_code` VARCHAR(64) NOT NULL COMMENT '通知类型编码',
+  `notice_type_name` VARCHAR(64) NOT NULL COMMENT '通知类型名称',
+  `push_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启PUSH推送:1是 0否',
+  `sms_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否开启短信通知:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_message_push_notice_type_code` (`notice_type_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息推送配置表';
+
+INSERT IGNORE INTO `tcm_message_push_config`
+(`id`, `notice_type_code`, `notice_type_name`, `push_enabled`, `sms_enabled`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, 'MEMBER_EXPIRE_REMIND', '会员到期提醒', 1, 1, 0, '系统初始化', '系统初始化'),
+(2, 'LIVE_REMIND', '直播提醒', 1, 1, 0, '系统初始化', '系统初始化'),
+(3, 'ACTIVITY_NOTICE', '活动通知', 1, 0, 0, '系统初始化', '系统初始化'),
+(4, 'ORDER_STATUS_UPDATE', '订单状态更新', 1, 0, 0, '系统初始化', '系统初始化'),
+(5, 'COURSE_UPDATE_NOTICE', '课程更新通知', 1, 0, 0, '系统初始化', '系统初始化'),
+(6, 'STUDY_PROGRESS_REMIND', '学习进度提醒', 1, 0, 0, '系统初始化', '系统初始化');
+
+CREATE TABLE IF NOT EXISTS `tcm_system_banner_config` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `banner_img` VARCHAR(512) NOT NULL COMMENT 'Banner图片',
+  `title` VARCHAR(64) NOT NULL COMMENT 'Banner标题',
+  `start_at` DATETIME NOT NULL COMMENT '展示开始时间',
+  `end_at` DATETIME NOT NULL COMMENT '展示结束时间',
+  `jump_type` VARCHAR(32) NOT NULL COMMENT '跳转类型',
+  `target_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '跳转目标ID',
+  `target_name` VARCHAR(64) DEFAULT NULL COMMENT '跳转目标名称',
+  `target_url` VARCHAR(512) DEFAULT NULL COMMENT '三方地址',
+  `publish_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否上架:1是 0否',
+  `deleted` TINYINT(1) NOT NULL DEFAULT 0,
+  `created_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '创建人',
+  `updated_by` VARCHAR(64) NOT NULL DEFAULT '系统初始化' COMMENT '更新人',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_system_banner_time` (`start_at`, `end_at`),
+  KEY `idx_system_banner_jump_type` (`jump_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Banner配置表';
+
+INSERT IGNORE INTO `tcm_system_banner_config`
+(`id`, `banner_img`, `title`, `start_at`, `end_at`, `jump_type`, `target_id`, `target_name`, `target_url`, `publish_enabled`, `deleted`, `created_by`, `updated_by`)
+VALUES
+(1, 'https://example.com/banner/member-discount.png', '会员显示折扣', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'MEMBER', NULL, '会员', NULL, 1, 0, '系统初始化', '系统初始化'),
+(2, 'https://example.com/banner/health-course.png', '兑换商城优惠券更新', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'HEALTH_COURSE', 1, '养生课程1', NULL, 1, 0, '系统初始化', '系统初始化'),
+(3, 'https://example.com/banner/third-party.png', '标题标题题', '2024-05-05 12:25:33', '2024-06-01 12:25:33', 'THIRD_PARTY_URL', NULL, NULL, 'https://www.baidu.com', 0, 0, '系统初始化', '系统初始化');
diff --git a/backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java b/backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
index 9b5e9f4..15e9a47 100644
--- a/backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
+++ b/backend/src/main/java/com/tcm/training/admin/bootstrap/DataInitializer.java
@@ -119,6 +119,40 @@
                 null, null, null, 4, "system:role:delete", "角色删除按钮"));
         menuIds.add(saveMenu(1300L, 1000L, "系统设置", "system_setting_group", "M",
                 "/system/settings", null, null, 30, null, "系统设置目录"));
+        menuIds.add(saveMenu(1305L, 1300L, "系统设置", "system_basic_settings", "C",
+                "/system/settings/basic", "System/BasicSettings", null, 5, "system:basic-settings:view", "系统设置菜单"));
+        menuIds.add(saveMenu(1306L, 1305L, "编辑系统设置", "system_basic_settings_edit", "B",
+                null, null, null, 1, "system:basic-settings:edit", "系统设置编辑按钮"));
+        menuIds.add(saveMenu(1307L, 1300L, "APP版本设置", "system_app_version_settings", "C",
+                "/system/settings/app-version", "System/AppVersionSettings", null, 15, "system:app-version:view", "APP版本设置菜单"));
+        menuIds.add(saveMenu(1308L, 1307L, "新增APP版本", "system_app_version_settings_add", "B",
+                null, null, null, 1, "system:app-version:add", "新增APP版本按钮"));
+        menuIds.add(saveMenu(1309L, 1307L, "查看APP版本", "system_app_version_settings_detail", "B",
+                null, null, null, 2, "system:app-version:detail", "查看APP版本按钮"));
+        menuIds.add(saveMenu(1312L, 1300L, "消息推送配置", "system_message_push_settings", "C",
+                "/system/settings/message-push", "System/MessagePushSettings", null, 20, "system:message-push:view", "消息推送配置菜单"));
+        menuIds.add(saveMenu(1313L, 1312L, "编辑消息推送配置", "system_message_push_settings_edit", "B",
+                null, null, null, 1, "system:message-push:edit", "消息推送配置编辑按钮"));
+        menuIds.add(saveMenu(1314L, 1300L, "Banner配置", "system_banner_settings", "C",
+                "/system/settings/banner", "System/BannerSettings", null, 12, "system:banner:view", "Banner配置菜单"));
+        menuIds.add(saveMenu(1315L, 1314L, "新增Banner", "system_banner_settings_add", "B",
+                null, null, null, 1, "system:banner:add", "Banner新增按钮"));
+        menuIds.add(saveMenu(1316L, 1314L, "编辑Banner", "system_banner_settings_edit", "B",
+                null, null, null, 2, "system:banner:edit", "Banner编辑按钮"));
+        menuIds.add(saveMenu(1317L, 1314L, "查看Banner", "system_banner_settings_detail", "B",
+                null, null, null, 3, "system:banner:detail", "Banner查看按钮"));
+        menuIds.add(saveMenu(1318L, 1314L, "删除Banner", "system_banner_settings_delete", "B",
+                null, null, null, 4, "system:banner:delete", "Banner删除按钮"));
+        menuIds.add(saveMenu(1319L, 1314L, "批量删除Banner", "system_banner_settings_batch_delete", "B",
+                null, null, null, 5, "system:banner:batch-delete", "Banner批量删除按钮"));
+        menuIds.add(saveMenu(1320L, 1314L, "切换Banner上架状态", "system_banner_settings_publish_status", "B",
+                null, null, null, 6, "system:banner:publish-status", "Banner上架状态按钮"));
+        menuIds.add(saveMenu(1321L, 1300L, "活动通知", "system_activity_notice", "C",
+                "/system/settings/activity-notice", "System/ActivityNotice", null, 13, "system:activity-notice:view", "活动通知菜单"));
+        menuIds.add(saveMenu(1322L, 1321L, "新增活动通知", "system_activity_notice_add", "B",
+                null, null, null, 1, "system:activity-notice:add", "活动通知新增按钮"));
+        menuIds.add(saveMenu(1323L, 1321L, "删除活动通知", "system_activity_notice_delete", "B",
+                null, null, null, 2, "system:activity-notice:delete", "活动通知删除按钮"));
         menuIds.add(saveMenu(1310L, 1300L, "积分设置", "system_points_settings", "C",
                 "/system/settings/points", "System/PointsSettings", null, 10, "system:points-settings:view", "积分设置菜单"));
         menuIds.add(saveMenu(1311L, 1310L, "编辑积分设置", "system_points_settings_edit", "B",
diff --git a/backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java b/backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
index 2df5ef6..091c90c 100644
--- a/backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
+++ b/backend/src/main/java/com/tcm/training/admin/config/SaTokenWebConfig.java
@@ -36,7 +36,12 @@
                     StpUtil.checkLogin();
                 }))
                 .addPathPatterns("/api/**", "/api/v1/**")
-                .excludePathPatterns("/api/auth/login","/api/auth/captcha");
+                .excludePathPatterns(
+                        "/api/auth/login",
+                        "/api/auth/captcha",
+                        "/api/app-version-settings/check",
+                        "/api/v1/app-version-settings/check"
+                );
 
         registry.addInterceptor(xssParamInterceptor)
                 .addPathPatterns("/api/**", "/api/v1/**");
diff --git a/backend/src/main/java/com/tcm/training/admin/system/controller/ActivityNoticeController.java b/backend/src/main/java/com/tcm/training/admin/system/controller/ActivityNoticeController.java
new file mode 100644
index 0000000..cbc23e1
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/controller/ActivityNoticeController.java
@@ -0,0 +1,49 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.system.dto.ActivityNoticeCreateRequest;
+import com.tcm.training.admin.system.service.ActivityNoticeAppService;
+import com.tcm.training.admin.system.vo.ActivityNoticeListItemVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class ActivityNoticeController {
+
+    private final ActivityNoticeAppService activityNoticeAppService;
+
+    public ActivityNoticeController(ActivityNoticeAppService activityNoticeAppService) {
+        this.activityNoticeAppService = activityNoticeAppService;
+    }
+
+    @GetMapping("/activity-notices")
+    public ApiResponse<PageResponse<ActivityNoticeListItemVO>> page(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "10") int pageSize,
+            @RequestParam(required = false) String title) {
+        return ApiResponse.ok(activityNoticeAppService.page(page, pageSize, title));
+    }
+
+    @PostMapping("/activity-notices")
+    public ApiResponse<ActivityNoticeListItemVO> create(@Valid @RequestBody ActivityNoticeCreateRequest request) {
+        return ApiResponse.ok(activityNoticeAppService.create(request));
+    }
+
+    @DeleteMapping("/activity-notices/{id}")
+    public ApiResponse<Void> delete(@PathVariable("id") Long id) {
+        activityNoticeAppService.delete(id);
+        return ApiResponse.ok(null);
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java b/backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java
new file mode 100644
index 0000000..aaf1162
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/controller/AppVersionSettingsController.java
@@ -0,0 +1,55 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.system.dto.AppVersionCreateRequest;
+import com.tcm.training.admin.system.service.AppVersionSettingsAppService;
+import com.tcm.training.admin.system.vo.AppVersionCheckVO;
+import com.tcm.training.admin.system.vo.AppVersionDetailVO;
+import com.tcm.training.admin.system.vo.AppVersionListItemVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class AppVersionSettingsController {
+
+    private final AppVersionSettingsAppService appVersionSettingsAppService;
+
+    public AppVersionSettingsController(AppVersionSettingsAppService appVersionSettingsAppService) {
+        this.appVersionSettingsAppService = appVersionSettingsAppService;
+    }
+
+    @GetMapping("/app-version-settings")
+    public ApiResponse<PageResponse<AppVersionListItemVO>> page(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "10") int pageSize) {
+        return ApiResponse.ok(appVersionSettingsAppService.page(page, pageSize));
+    }
+
+    @GetMapping("/app-version-settings/check")
+    public ApiResponse<AppVersionCheckVO> checkUpdate(
+            @RequestParam String platform,
+            @RequestParam Integer currentVersionCode) {
+        return ApiResponse.ok(appVersionSettingsAppService.checkUpdate(platform, currentVersionCode));
+    }
+
+    @GetMapping("/app-version-settings/{id}")
+    public ApiResponse<AppVersionDetailVO> detail(@PathVariable("id") Long id) {
+        return ApiResponse.ok(appVersionSettingsAppService.detail(id));
+    }
+
+    @PostMapping("/app-version-settings")
+    public ApiResponse<AppVersionDetailVO> create(@Valid @RequestBody AppVersionCreateRequest request) {
+        return ApiResponse.ok(appVersionSettingsAppService.create(request));
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/controller/MessagePushSettingsController.java b/backend/src/main/java/com/tcm/training/admin/system/controller/MessagePushSettingsController.java
new file mode 100644
index 0000000..742ee23
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/controller/MessagePushSettingsController.java
@@ -0,0 +1,37 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.system.dto.MessagePushSettingsSaveRequest;
+import com.tcm.training.admin.system.service.MessagePushSettingsAppService;
+import com.tcm.training.admin.system.vo.MessagePushSettingVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class MessagePushSettingsController {
+
+    private final MessagePushSettingsAppService messagePushSettingsAppService;
+
+    public MessagePushSettingsController(MessagePushSettingsAppService messagePushSettingsAppService) {
+        this.messagePushSettingsAppService = messagePushSettingsAppService;
+    }
+
+    @GetMapping("/message-push-settings")
+    public ApiResponse<List<MessagePushSettingVO>> current() {
+        return ApiResponse.ok(messagePushSettingsAppService.current());
+    }
+
+    @PutMapping("/message-push-settings")
+    public ApiResponse<List<MessagePushSettingVO>> saveCurrent(@Valid @RequestBody MessagePushSettingsSaveRequest request) {
+        return ApiResponse.ok(messagePushSettingsAppService.saveCurrent(request));
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/controller/SystemBannerController.java b/backend/src/main/java/com/tcm/training/admin/system/controller/SystemBannerController.java
new file mode 100644
index 0000000..69915e2
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/controller/SystemBannerController.java
@@ -0,0 +1,91 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.system.dto.SystemBannerBatchDeleteRequest;
+import com.tcm.training.admin.system.dto.SystemBannerPublishStatusRequest;
+import com.tcm.training.admin.system.dto.SystemBannerSaveRequest;
+import com.tcm.training.admin.system.service.SystemBannerAppService;
+import com.tcm.training.admin.system.vo.SystemBannerCourseOptionVO;
+import com.tcm.training.admin.system.vo.SystemBannerDetailVO;
+import com.tcm.training.admin.system.vo.SystemBannerListItemVO;
+import com.tcm.training.admin.system.vo.SystemBannerPublishStatusVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PatchMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class SystemBannerController {
+
+    private final SystemBannerAppService systemBannerAppService;
+
+    public SystemBannerController(SystemBannerAppService systemBannerAppService) {
+        this.systemBannerAppService = systemBannerAppService;
+    }
+
+    @GetMapping("/system-banners")
+    public ApiResponse<PageResponse<SystemBannerListItemVO>> page(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "10") int pageSize,
+            @RequestParam(required = false) String title,
+            @RequestParam(required = false) Integer status) {
+        return ApiResponse.ok(systemBannerAppService.page(page, pageSize, title, status));
+    }
+
+    @GetMapping("/system-banners/{id}")
+    public ApiResponse<SystemBannerDetailVO> detail(@PathVariable("id") Long id) {
+        return ApiResponse.ok(systemBannerAppService.detail(id));
+    }
+
+    @PostMapping("/system-banners")
+    public ApiResponse<SystemBannerDetailVO> create(@Valid @RequestBody SystemBannerSaveRequest request) {
+        return ApiResponse.ok(systemBannerAppService.create(request));
+    }
+
+    @PutMapping("/system-banners/{id}")
+    public ApiResponse<SystemBannerDetailVO> update(@PathVariable("id") Long id,
+                                                    @Valid @RequestBody SystemBannerSaveRequest request) {
+        return ApiResponse.ok(systemBannerAppService.update(id, request));
+    }
+
+    @DeleteMapping("/system-banners/{id}")
+    public ApiResponse<Void> delete(@PathVariable("id") Long id) {
+        systemBannerAppService.delete(id);
+        return ApiResponse.ok(null);
+    }
+
+    @DeleteMapping("/system-banners/batch")
+    public ApiResponse<Void> batchDelete(@Valid @RequestBody SystemBannerBatchDeleteRequest request) {
+        systemBannerAppService.batchDelete(request);
+        return ApiResponse.ok(null);
+    }
+
+    @PatchMapping("/system-banners/{id}/publish-status")
+    public ApiResponse<SystemBannerPublishStatusVO> patchPublishStatus(@PathVariable("id") Long id,
+                                                                       @Valid @RequestBody SystemBannerPublishStatusRequest request) {
+        return ApiResponse.ok(systemBannerAppService.patchPublishStatus(id, request));
+    }
+
+    @GetMapping("/system-banners/health-course-options")
+    public ApiResponse<List<SystemBannerCourseOptionVO>> healthCourseOptions(@RequestParam(required = false) String keyword) {
+        return ApiResponse.ok(systemBannerAppService.healthCourseOptions(keyword));
+    }
+
+    @GetMapping("/system-banners/live-course-options")
+    public ApiResponse<List<SystemBannerCourseOptionVO>> liveCourseOptions(@RequestParam(required = false) String keyword) {
+        return ApiResponse.ok(systemBannerAppService.liveCourseOptions(keyword));
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/controller/SystemSettingsController.java b/backend/src/main/java/com/tcm/training/admin/system/controller/SystemSettingsController.java
new file mode 100644
index 0000000..bc1ab99
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/controller/SystemSettingsController.java
@@ -0,0 +1,36 @@
+package com.tcm.training.admin.system.controller;
+
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.system.dto.SystemSettingsSaveRequest;
+import com.tcm.training.admin.system.service.SystemSettingsAppService;
+import com.tcm.training.admin.system.vo.SystemSettingsVO;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@Validated
+@RestController
+@RequestMapping({"/api/v1", "/api"})
+public class SystemSettingsController {
+
+    private final SystemSettingsAppService systemSettingsAppService;
+
+    public SystemSettingsController(SystemSettingsAppService systemSettingsAppService) {
+        this.systemSettingsAppService = systemSettingsAppService;
+    }
+
+    @GetMapping("/system-settings/current")
+    public ApiResponse<SystemSettingsVO> current() {
+        return ApiResponse.ok(systemSettingsAppService.current());
+    }
+
+    @PutMapping("/system-settings/current")
+    public ApiResponse<SystemSettingsVO> saveCurrent(@Valid @RequestBody SystemSettingsSaveRequest request) {
+        return ApiResponse.ok(systemSettingsAppService.saveCurrent(request));
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/ActivityNoticeCreateRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/ActivityNoticeCreateRequest.java
new file mode 100644
index 0000000..88d2319
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/ActivityNoticeCreateRequest.java
@@ -0,0 +1,21 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class ActivityNoticeCreateRequest {
+
+    @NotBlank(message = "请输入通知标题")
+    @Size(max = 15, message = "通知标题长度不能超过15个字符")
+    private String title;
+
+    @NotBlank(message = "请输入通知内容")
+    private String contentHtml;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java
new file mode 100644
index 0000000..97f854e
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/AppVersionCreateRequest.java
@@ -0,0 +1,42 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppVersionCreateRequest {
+
+    @NotBlank(message = "请输入版本号")
+    @Size(max = 32, message = "版本号长度不能超过32个字符")
+    private String versionNo;
+
+    @NotBlank(message = "请上传安卓文件")
+    @Size(max = 512, message = "安卓文件地址长度不能超过512个字符")
+    private String androidFileUrl;
+
+    @NotBlank(message = "请填写安卓文件名称")
+    @Size(max = 255, message = "安卓文件名称长度不能超过255个字符")
+    private String androidFileName;
+
+    @NotBlank(message = "请上传iOS文件")
+    @Size(max = 512, message = "iOS文件地址长度不能超过512个字符")
+    private String iosFileUrl;
+
+    @NotBlank(message = "请填写iOS文件名称")
+    @Size(max = 255, message = "iOS文件名称长度不能超过255个字符")
+    private String iosFileName;
+
+    @NotNull(message = "请选择是否强制更新")
+    private Boolean forceUpdate;
+
+    @NotBlank(message = "请输入更新说明")
+    @Size(max = 2000, message = "更新说明长度不能超过2000个字符")
+    private String updateNotice;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingItemSaveRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingItemSaveRequest.java
new file mode 100644
index 0000000..e234a8c
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingItemSaveRequest.java
@@ -0,0 +1,25 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class MessagePushSettingItemSaveRequest {
+
+    @NotBlank(message = "通知类型不能为空")
+    @Size(max = 64, message = "通知类型编码长度不能超过64个字符")
+    private String noticeTypeCode;
+
+    @NotNull(message = "请设置PUSH推送")
+    private Boolean pushEnabled;
+
+    @NotNull(message = "请设置短信通知")
+    private Boolean smsEnabled;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingsSaveRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingsSaveRequest.java
new file mode 100644
index 0000000..f578ac1
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/MessagePushSettingsSaveRequest.java
@@ -0,0 +1,19 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class MessagePushSettingsSaveRequest {
+
+    @Valid
+    @NotEmpty(message = "消息推送配置不能为空")
+    private List<MessagePushSettingItemSaveRequest> items;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerBatchDeleteRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerBatchDeleteRequest.java
new file mode 100644
index 0000000..7f4bcf9
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerBatchDeleteRequest.java
@@ -0,0 +1,18 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerBatchDeleteRequest {
+
+    @NotEmpty(message = "请选择要删除的banner")
+    private List<@NotNull(message = "banner标识不能为空") Long> ids;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerPublishStatusRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerPublishStatusRequest.java
new file mode 100644
index 0000000..75a8926
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerPublishStatusRequest.java
@@ -0,0 +1,16 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerPublishStatusRequest {
+
+    @NotNull(message = "请选择banner上架状态")
+    private Boolean publishEnabled;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerSaveRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerSaveRequest.java
new file mode 100644
index 0000000..6bd4ab2
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemBannerSaveRequest.java
@@ -0,0 +1,45 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerSaveRequest {
+
+    @NotBlank(message = "请上传banner图")
+    @Size(max = 512, message = "banner图地址长度不能超过512个字符")
+    private String bannerImg;
+
+    @NotBlank(message = "请输入banner标题")
+    @Size(max = 64, message = "banner标题长度不能超过64个字符")
+    private String title;
+
+    @NotNull(message = "请选择banner开始时间")
+    private LocalDateTime startAt;
+
+    @NotNull(message = "请选择banner结束时间")
+    private LocalDateTime endAt;
+
+    @NotBlank(message = "请选择跳转类型")
+    @Size(max = 32, message = "跳转类型长度不能超过32个字符")
+    private String jumpType;
+
+    private Long targetId;
+
+    @Size(max = 64, message = "目标名称长度不能超过64个字符")
+    private String targetName;
+
+    @Size(max = 512, message = "地址信息长度不能超过512个字符")
+    private String targetUrl;
+
+    @NotNull(message = "请选择banner上架状态")
+    private Boolean publishEnabled;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/dto/SystemSettingsSaveRequest.java b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemSettingsSaveRequest.java
new file mode 100644
index 0000000..462927f
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/dto/SystemSettingsSaveRequest.java
@@ -0,0 +1,32 @@
+package com.tcm.training.admin.system.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemSettingsSaveRequest {
+
+    @NotBlank(message = "请输入客服电话")
+    @Size(max = 32, message = "客服电话长度不能超过32个字符")
+    private String servicePhone;
+
+    @NotBlank(message = "请输入客服在线时间")
+    @Size(max = 64, message = "客服在线时间长度不能超过64个字符")
+    private String serviceOnlineTime;
+
+    @NotNull(message = "请输入备份周期")
+    @Min(value = 1, message = "备份周期必须大于0")
+    private Integer backupCycleDays;
+
+    @NotNull(message = "请输入会员到期提醒天数")
+    @Min(value = 1, message = "会员到期提醒天数必须大于0")
+    private Integer memberExpireRemindDays;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/entity/ActivityNotice.java b/backend/src/main/java/com/tcm/training/admin/system/entity/ActivityNotice.java
new file mode 100644
index 0000000..54e2af0
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/entity/ActivityNotice.java
@@ -0,0 +1,44 @@
+package com.tcm.training.admin.system.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@TableName("tcm_activity_notice")
+@Getter
+@Setter
+@NoArgsConstructor
+public class ActivityNotice {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("title")
+    private String title;
+
+    @TableField("content_html")
+    private String contentHtml;
+
+    @TableLogic
+    private Integer deleted;
+
+    @TableField(value = "created_by", fill = FieldFill.INSERT)
+    private String createdBy;
+
+    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/entity/AppVersionConfig.java b/backend/src/main/java/com/tcm/training/admin/system/entity/AppVersionConfig.java
new file mode 100644
index 0000000..2195dff
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/entity/AppVersionConfig.java
@@ -0,0 +1,65 @@
+package com.tcm.training.admin.system.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@TableName("tcm_app_version_config")
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppVersionConfig {
+
+    public static final String PLATFORM_ANDROID = "ANDROID";
+    public static final String PLATFORM_IOS = "IOS";
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("version_no")
+    private String versionNo;
+
+    @TableField("version_code")
+    private Integer versionCode;
+
+    @TableField("android_file_url")
+    private String androidFileUrl;
+
+    @TableField("android_file_name")
+    private String androidFileName;
+
+    @TableField("ios_file_url")
+    private String iosFileUrl;
+
+    @TableField("ios_file_name")
+    private String iosFileName;
+
+    @TableField("force_update")
+    private Integer forceUpdate;
+
+    @TableField("update_notice")
+    private String updateNotice;
+
+    @TableLogic
+    private Integer deleted;
+
+    @TableField(value = "created_by", fill = FieldFill.INSERT)
+    private String createdBy;
+
+    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/entity/MessagePushConfig.java b/backend/src/main/java/com/tcm/training/admin/system/entity/MessagePushConfig.java
new file mode 100644
index 0000000..b2b7923
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/entity/MessagePushConfig.java
@@ -0,0 +1,50 @@
+package com.tcm.training.admin.system.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@TableName("tcm_message_push_config")
+@Getter
+@Setter
+@NoArgsConstructor
+public class MessagePushConfig {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("notice_type_code")
+    private String noticeTypeCode;
+
+    @TableField("notice_type_name")
+    private String noticeTypeName;
+
+    @TableField("push_enabled")
+    private Integer pushEnabled;
+
+    @TableField("sms_enabled")
+    private Integer smsEnabled;
+
+    @TableLogic
+    private Integer deleted;
+
+    @TableField(value = "created_by", fill = FieldFill.INSERT)
+    private String createdBy;
+
+    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/entity/SystemBannerConfig.java b/backend/src/main/java/com/tcm/training/admin/system/entity/SystemBannerConfig.java
new file mode 100644
index 0000000..00a002c
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/entity/SystemBannerConfig.java
@@ -0,0 +1,74 @@
+package com.tcm.training.admin.system.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@TableName("tcm_system_banner_config")
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerConfig {
+
+    public static final String JUMP_TYPE_HEALTH_COURSE = "HEALTH_COURSE";
+    public static final String JUMP_TYPE_LIVE_COURSE = "LIVE_COURSE";
+    public static final String JUMP_TYPE_MEMBER = "MEMBER";
+    public static final String JUMP_TYPE_THIRD_PARTY_URL = "THIRD_PARTY_URL";
+
+    public static final int DISPLAY_STATUS_NOT_STARTED = 1;
+    public static final int DISPLAY_STATUS_ONGOING = 2;
+    public static final int DISPLAY_STATUS_ENDED = 3;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("banner_img")
+    private String bannerImg;
+
+    @TableField("title")
+    private String title;
+
+    @TableField("start_at")
+    private LocalDateTime startAt;
+
+    @TableField("end_at")
+    private LocalDateTime endAt;
+
+    @TableField("jump_type")
+    private String jumpType;
+
+    @TableField("target_id")
+    private Long targetId;
+
+    @TableField("target_name")
+    private String targetName;
+
+    @TableField("target_url")
+    private String targetUrl;
+
+    @TableField("publish_enabled")
+    private Integer publishEnabled;
+
+    @TableLogic
+    private Integer deleted;
+
+    @TableField(value = "created_by", fill = FieldFill.INSERT)
+    private String createdBy;
+
+    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/entity/SystemSettings.java b/backend/src/main/java/com/tcm/training/admin/system/entity/SystemSettings.java
new file mode 100644
index 0000000..53c3566
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/entity/SystemSettings.java
@@ -0,0 +1,50 @@
+package com.tcm.training.admin.system.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@TableName("tcm_system_settings")
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemSettings {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("service_phone")
+    private String servicePhone;
+
+    @TableField("service_online_time")
+    private String serviceOnlineTime;
+
+    @TableField("backup_cycle_days")
+    private Integer backupCycleDays;
+
+    @TableField("member_expire_remind_days")
+    private Integer memberExpireRemindDays;
+
+    @TableLogic
+    private Integer deleted;
+
+    @TableField(value = "created_by", fill = FieldFill.INSERT)
+    private String createdBy;
+
+    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
+    private String updatedBy;
+
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/mapper/ActivityNoticeMapper.java b/backend/src/main/java/com/tcm/training/admin/system/mapper/ActivityNoticeMapper.java
new file mode 100644
index 0000000..15e3b3e
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/mapper/ActivityNoticeMapper.java
@@ -0,0 +1,9 @@
+package com.tcm.training.admin.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.system.entity.ActivityNotice;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ActivityNoticeMapper extends BaseMapper<ActivityNotice> {
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/mapper/AppVersionConfigMapper.java b/backend/src/main/java/com/tcm/training/admin/system/mapper/AppVersionConfigMapper.java
new file mode 100644
index 0000000..b558e33
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/mapper/AppVersionConfigMapper.java
@@ -0,0 +1,9 @@
+package com.tcm.training.admin.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.system.entity.AppVersionConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AppVersionConfigMapper extends BaseMapper<AppVersionConfig> {
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/mapper/MessagePushConfigMapper.java b/backend/src/main/java/com/tcm/training/admin/system/mapper/MessagePushConfigMapper.java
new file mode 100644
index 0000000..b70c53c
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/mapper/MessagePushConfigMapper.java
@@ -0,0 +1,9 @@
+package com.tcm.training.admin.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.system.entity.MessagePushConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface MessagePushConfigMapper extends BaseMapper<MessagePushConfig> {
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemBannerConfigMapper.java b/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemBannerConfigMapper.java
new file mode 100644
index 0000000..357a5ee
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemBannerConfigMapper.java
@@ -0,0 +1,9 @@
+package com.tcm.training.admin.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.system.entity.SystemBannerConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SystemBannerConfigMapper extends BaseMapper<SystemBannerConfig> {
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemSettingsMapper.java b/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemSettingsMapper.java
new file mode 100644
index 0000000..4f53a01
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/mapper/SystemSettingsMapper.java
@@ -0,0 +1,9 @@
+package com.tcm.training.admin.system.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.system.entity.SystemSettings;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SystemSettingsMapper extends BaseMapper<SystemSettings> {
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/service/ActivityNoticeAppService.java b/backend/src/main/java/com/tcm/training/admin/system/service/ActivityNoticeAppService.java
new file mode 100644
index 0000000..8cbd9c5
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/service/ActivityNoticeAppService.java
@@ -0,0 +1,116 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.system.dto.ActivityNoticeCreateRequest;
+import com.tcm.training.admin.system.entity.ActivityNotice;
+import com.tcm.training.admin.system.mapper.ActivityNoticeMapper;
+import com.tcm.training.admin.system.vo.ActivityNoticeListItemVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class ActivityNoticeAppService {
+
+    private final ActivityNoticeMapper activityNoticeMapper;
+
+    public ActivityNoticeAppService(ActivityNoticeMapper activityNoticeMapper) {
+        this.activityNoticeMapper = activityNoticeMapper;
+    }
+
+    public PageResponse<ActivityNoticeListItemVO> page(int page, int pageSize, String title) {
+        LambdaQueryWrapper<ActivityNotice> qw = new LambdaQueryWrapper<ActivityNotice>()
+                .eq(ActivityNotice::getDeleted, 0)
+                .like(title != null && !title.trim().isEmpty(), ActivityNotice::getTitle, title == null ? null : title.trim())
+                .orderByDesc(ActivityNotice::getCreatedAt)
+                .orderByDesc(ActivityNotice::getId);
+        IPage<ActivityNotice> result = activityNoticeMapper.selectPage(new Page<ActivityNotice>(page, pageSize), qw);
+        List<ActivityNoticeListItemVO> list = new ArrayList<ActivityNoticeListItemVO>();
+        for (ActivityNotice item : result.getRecords()) {
+            list.add(toListItem(item));
+        }
+        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    @Transactional
+    public ActivityNoticeListItemVO create(ActivityNoticeCreateRequest request) {
+        ActivityNotice item = new ActivityNotice();
+        item.setTitle(normalizeTitle(request.getTitle()));
+        item.setContentHtml(normalizeContent(request.getContentHtml()));
+        item.setDeleted(0);
+        activityNoticeMapper.insert(item);
+        return toListItem(requireItem(item.getId()));
+    }
+
+    @Transactional
+    public void delete(Long id) {
+        requireItem(id);
+        activityNoticeMapper.deleteById(id);
+    }
+
+    private ActivityNotice requireItem(Long id) {
+        ActivityNotice item = activityNoticeMapper.selectOne(new LambdaQueryWrapper<ActivityNotice>()
+                .eq(ActivityNotice::getId, id)
+                .eq(ActivityNotice::getDeleted, 0)
+                .last("limit 1"));
+        if (item == null) {
+            throw new IllegalArgumentException("活动通知不存在");
+        }
+        return item;
+    }
+
+    private ActivityNoticeListItemVO toListItem(ActivityNotice item) {
+        ActivityNoticeListItemVO vo = new ActivityNoticeListItemVO();
+        vo.setId(item.getId());
+        vo.setTitle(item.getTitle());
+        vo.setCreatedAt(item.getCreatedAt());
+        return vo;
+    }
+
+    private String normalizeTitle(String title) {
+        String trimmed = trimToNull(title);
+        if (trimmed == null) {
+            throw new IllegalArgumentException("请输入通知标题");
+        }
+        return trimmed;
+    }
+
+    private String normalizeContent(String contentHtml) {
+        String trimmed = trimToNull(contentHtml);
+        if (trimmed == null || isBlankRichText(trimmed)) {
+            throw new IllegalArgumentException("请输入通知内容");
+        }
+        return trimmed;
+    }
+
+    private boolean isBlankRichText(String html) {
+        String normalized = html
+                .replaceAll("(?i)<br\\s*/?>", "")
+                .replaceAll("(?i)</p>", "")
+                .replace("&nbsp;", "")
+                .replace("&#160;", "")
+                .replaceAll("<[^>]+>", "")
+                .trim();
+        if (!normalized.isEmpty()) {
+            return false;
+        }
+        String lowerCaseHtml = html.toLowerCase();
+        return !lowerCaseHtml.contains("<img")
+                && !lowerCaseHtml.contains("<video")
+                && !lowerCaseHtml.contains("<audio")
+                && !lowerCaseHtml.contains("<iframe");
+    }
+
+    private String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java b/backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
new file mode 100644
index 0000000..cb70f01
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/service/AppVersionSettingsAppService.java
@@ -0,0 +1,154 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.system.dto.AppVersionCreateRequest;
+import com.tcm.training.admin.system.entity.AppVersionConfig;
+import com.tcm.training.admin.system.mapper.AppVersionConfigMapper;
+import com.tcm.training.admin.system.vo.AppVersionCheckVO;
+import com.tcm.training.admin.system.vo.AppVersionDetailVO;
+import com.tcm.training.admin.system.vo.AppVersionListItemVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+public class AppVersionSettingsAppService {
+
+    private final AppVersionConfigMapper appVersionConfigMapper;
+
+    public AppVersionSettingsAppService(AppVersionConfigMapper appVersionConfigMapper) {
+        this.appVersionConfigMapper = appVersionConfigMapper;
+    }
+
+    public PageResponse<AppVersionListItemVO> page(int page, int pageSize) {
+        LambdaQueryWrapper<AppVersionConfig> qw = new LambdaQueryWrapper<AppVersionConfig>()
+                .eq(AppVersionConfig::getDeleted, 0)
+                .orderByDesc(AppVersionConfig::getVersionCode)
+                .orderByDesc(AppVersionConfig::getId);
+        IPage<AppVersionConfig> result = appVersionConfigMapper.selectPage(new Page<AppVersionConfig>(page, pageSize), qw);
+        List<AppVersionListItemVO> list = new ArrayList<AppVersionListItemVO>();
+        for (AppVersionConfig item : result.getRecords()) {
+            list.add(toListItem(item));
+        }
+        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    public AppVersionDetailVO detail(Long id) {
+        return toDetailVO(requireItem(id));
+    }
+
+    @Transactional
+    public AppVersionDetailVO create(AppVersionCreateRequest request) {
+        AppVersionConfig latest = latest();
+        AppVersionConfig item = new AppVersionConfig();
+        item.setVersionNo(request.getVersionNo().trim());
+        item.setVersionCode(latest == null ? 1 : defaultNumber(latest.getVersionCode()) + 1);
+        item.setAndroidFileUrl(request.getAndroidFileUrl().trim());
+        item.setAndroidFileName(request.getAndroidFileName().trim());
+        item.setIosFileUrl(request.getIosFileUrl().trim());
+        item.setIosFileName(request.getIosFileName().trim());
+        item.setForceUpdate(booleanToInteger(request.getForceUpdate()));
+        item.setUpdateNotice(request.getUpdateNotice().trim());
+        item.setDeleted(0);
+        appVersionConfigMapper.insert(item);
+        return toDetailVO(item);
+    }
+
+    public AppVersionCheckVO checkUpdate(String platform, Integer currentVersionCode) {
+        if (currentVersionCode == null) {
+            throw new IllegalArgumentException("当前版本号不能为空");
+        }
+        if (currentVersionCode < 0) {
+            throw new IllegalArgumentException("当前版本号不能小于0");
+        }
+        String normalizedPlatform = normalizePlatform(platform);
+        AppVersionConfig latest = latest();
+        AppVersionCheckVO vo = new AppVersionCheckVO();
+        if (latest == null) {
+            vo.setHasUpdate(Boolean.FALSE);
+            return vo;
+        }
+
+        boolean hasUpdate = defaultNumber(latest.getVersionCode()) > currentVersionCode;
+        vo.setHasUpdate(hasUpdate);
+        vo.setLatestVersionNo(latest.getVersionNo());
+        vo.setLatestVersionCode(defaultNumber(latest.getVersionCode()));
+        vo.setForceUpdate(integerToBoolean(latest.getForceUpdate()));
+        vo.setUpdateNotice(latest.getUpdateNotice());
+        vo.setDownloadUrl(AppVersionConfig.PLATFORM_ANDROID.equals(normalizedPlatform)
+                ? latest.getAndroidFileUrl()
+                : latest.getIosFileUrl());
+        return vo;
+    }
+
+    private AppVersionConfig latest() {
+        return appVersionConfigMapper.selectOne(new LambdaQueryWrapper<AppVersionConfig>()
+                .eq(AppVersionConfig::getDeleted, 0)
+                .orderByDesc(AppVersionConfig::getVersionCode)
+                .orderByDesc(AppVersionConfig::getId)
+                .last("limit 1"));
+    }
+
+    private AppVersionConfig requireItem(Long id) {
+        AppVersionConfig item = appVersionConfigMapper.selectOne(new LambdaQueryWrapper<AppVersionConfig>()
+                .eq(AppVersionConfig::getId, id)
+                .eq(AppVersionConfig::getDeleted, 0)
+                .last("limit 1"));
+        if (item == null) {
+            throw new IllegalArgumentException("APP版本不存在");
+        }
+        return item;
+    }
+
+    private String normalizePlatform(String platform) {
+        if (platform == null) {
+            throw new IllegalArgumentException("平台类型不能为空");
+        }
+        String normalized = platform.trim().toUpperCase();
+        if (AppVersionConfig.PLATFORM_ANDROID.equals(normalized) || AppVersionConfig.PLATFORM_IOS.equals(normalized)) {
+            return normalized;
+        }
+        throw new IllegalArgumentException("平台类型不合法");
+    }
+
+    private AppVersionListItemVO toListItem(AppVersionConfig item) {
+        AppVersionListItemVO vo = new AppVersionListItemVO();
+        vo.setId(item.getId());
+        vo.setVersionNo(item.getVersionNo());
+        vo.setUpdateNotice(item.getUpdateNotice());
+        vo.setCreatedAt(item.getCreatedAt());
+        return vo;
+    }
+
+    private AppVersionDetailVO toDetailVO(AppVersionConfig item) {
+        AppVersionDetailVO vo = new AppVersionDetailVO();
+        vo.setId(item.getId());
+        vo.setVersionNo(item.getVersionNo());
+        vo.setVersionCode(defaultNumber(item.getVersionCode()));
+        vo.setAndroidFileUrl(item.getAndroidFileUrl());
+        vo.setAndroidFileName(item.getAndroidFileName());
+        vo.setIosFileUrl(item.getIosFileUrl());
+        vo.setIosFileName(item.getIosFileName());
+        vo.setForceUpdate(integerToBoolean(item.getForceUpdate()));
+        vo.setUpdateNotice(item.getUpdateNotice());
+        vo.setCreatedAt(item.getCreatedAt());
+        return vo;
+    }
+
+    private Integer defaultNumber(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+    private Integer booleanToInteger(Boolean value) {
+        return Boolean.TRUE.equals(value) ? 1 : 0;
+    }
+
+    private Boolean integerToBoolean(Integer value) {
+        return value != null && value == 1;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/service/MessagePushSettingsAppService.java b/backend/src/main/java/com/tcm/training/admin/system/service/MessagePushSettingsAppService.java
new file mode 100644
index 0000000..4d73fa1
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/service/MessagePushSettingsAppService.java
@@ -0,0 +1,190 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tcm.training.admin.system.dto.MessagePushSettingItemSaveRequest;
+import com.tcm.training.admin.system.dto.MessagePushSettingsSaveRequest;
+import com.tcm.training.admin.system.entity.MessagePushConfig;
+import com.tcm.training.admin.system.mapper.MessagePushConfigMapper;
+import com.tcm.training.admin.system.vo.MessagePushSettingVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Service
+public class MessagePushSettingsAppService {
+
+    private static final List<MessagePushTypeDefinition> TYPE_DEFINITIONS = Arrays.asList(
+            new MessagePushTypeDefinition("MEMBER_EXPIRE_REMIND", "会员到期提醒", true, true),
+            new MessagePushTypeDefinition("LIVE_REMIND", "直播提醒", true, true),
+            new MessagePushTypeDefinition("ACTIVITY_NOTICE", "活动通知", true, false),
+            new MessagePushTypeDefinition("ORDER_STATUS_UPDATE", "订单状态更新", true, false),
+            new MessagePushTypeDefinition("COURSE_UPDATE_NOTICE", "课程更新通知", true, false),
+            new MessagePushTypeDefinition("STUDY_PROGRESS_REMIND", "学习进度提醒", true, false)
+    );
+
+    private final MessagePushConfigMapper messagePushConfigMapper;
+
+    public MessagePushSettingsAppService(MessagePushConfigMapper messagePushConfigMapper) {
+        this.messagePushConfigMapper = messagePushConfigMapper;
+    }
+
+    @Transactional
+    public List<MessagePushSettingVO> current() {
+        ensureDefaults();
+        return loadCurrentList();
+    }
+
+    @Transactional
+    public List<MessagePushSettingVO> saveCurrent(MessagePushSettingsSaveRequest request) {
+        ensureDefaults();
+        Map<String, MessagePushSettingItemSaveRequest> requestMap = normalizeRequest(request);
+        Map<String, MessagePushConfig> currentMap = currentConfigMap();
+        for (MessagePushTypeDefinition definition : TYPE_DEFINITIONS) {
+            MessagePushSettingItemSaveRequest itemRequest = requestMap.get(definition.getCode());
+            MessagePushConfig config = currentMap.get(definition.getCode());
+            if (config == null) {
+                config = createDefaultConfig(definition);
+                messagePushConfigMapper.insert(config);
+            }
+            config.setPushEnabled(booleanToInteger(itemRequest.getPushEnabled()));
+            config.setSmsEnabled(booleanToInteger(itemRequest.getSmsEnabled()));
+            messagePushConfigMapper.updateById(config);
+        }
+        return loadCurrentList();
+    }
+
+    private void ensureDefaults() {
+        Map<String, MessagePushConfig> currentMap = currentConfigMap();
+        for (MessagePushTypeDefinition definition : TYPE_DEFINITIONS) {
+            if (!currentMap.containsKey(definition.getCode())) {
+                MessagePushConfig config = createDefaultConfig(definition);
+                messagePushConfigMapper.insert(config);
+            }
+        }
+    }
+
+    private MessagePushConfig createDefaultConfig(MessagePushTypeDefinition definition) {
+        MessagePushConfig config = new MessagePushConfig();
+        config.setNoticeTypeCode(definition.getCode());
+        config.setNoticeTypeName(definition.getName());
+        config.setPushEnabled(booleanToInteger(definition.getDefaultPushEnabled()));
+        config.setSmsEnabled(booleanToInteger(definition.getDefaultSmsEnabled()));
+        config.setDeleted(0);
+        return config;
+    }
+
+    private Map<String, MessagePushSettingItemSaveRequest> normalizeRequest(MessagePushSettingsSaveRequest request) {
+        Map<String, MessagePushSettingItemSaveRequest> requestMap = new LinkedHashMap<String, MessagePushSettingItemSaveRequest>();
+        Set<String> expectedCodes = typeCodeSet();
+        for (MessagePushSettingItemSaveRequest item : request.getItems()) {
+            String code = item.getNoticeTypeCode().trim();
+            if (!expectedCodes.contains(code)) {
+                throw new IllegalArgumentException("通知类型不合法");
+            }
+            if (requestMap.containsKey(code)) {
+                throw new IllegalArgumentException("通知类型不能重复");
+            }
+            item.setNoticeTypeCode(code);
+            requestMap.put(code, item);
+        }
+        if (requestMap.size() != TYPE_DEFINITIONS.size()) {
+            throw new IllegalArgumentException("消息推送配置项不完整");
+        }
+        for (MessagePushTypeDefinition definition : TYPE_DEFINITIONS) {
+            if (!requestMap.containsKey(definition.getCode())) {
+                throw new IllegalArgumentException("消息推送配置项不完整");
+            }
+        }
+        return requestMap;
+    }
+
+    private List<MessagePushSettingVO> loadCurrentList() {
+        Map<String, MessagePushConfig> currentMap = currentConfigMap();
+        List<MessagePushSettingVO> list = new ArrayList<MessagePushSettingVO>();
+        for (MessagePushTypeDefinition definition : TYPE_DEFINITIONS) {
+            MessagePushConfig config = currentMap.get(definition.getCode());
+            if (config != null) {
+                list.add(toVO(config));
+            }
+        }
+        return list;
+    }
+
+    private Map<String, MessagePushConfig> currentConfigMap() {
+        List<MessagePushConfig> list = messagePushConfigMapper.selectList(new LambdaQueryWrapper<MessagePushConfig>()
+                .eq(MessagePushConfig::getDeleted, 0)
+                .orderByAsc(MessagePushConfig::getId));
+        Map<String, MessagePushConfig> map = new LinkedHashMap<String, MessagePushConfig>();
+        for (MessagePushConfig item : list) {
+            if (!map.containsKey(item.getNoticeTypeCode())) {
+                map.put(item.getNoticeTypeCode(), item);
+            }
+        }
+        return map;
+    }
+
+    private Set<String> typeCodeSet() {
+        Set<String> codes = new LinkedHashSet<String>();
+        for (MessagePushTypeDefinition definition : TYPE_DEFINITIONS) {
+            codes.add(definition.getCode());
+        }
+        return codes;
+    }
+
+    private MessagePushSettingVO toVO(MessagePushConfig config) {
+        MessagePushSettingVO vo = new MessagePushSettingVO();
+        vo.setId(config.getId());
+        vo.setNoticeTypeCode(config.getNoticeTypeCode());
+        vo.setNoticeTypeName(config.getNoticeTypeName());
+        vo.setPushEnabled(integerToBoolean(config.getPushEnabled()));
+        vo.setSmsEnabled(integerToBoolean(config.getSmsEnabled()));
+        vo.setUpdatedAt(config.getUpdatedAt());
+        return vo;
+    }
+
+    private Integer booleanToInteger(Boolean value) {
+        return Boolean.TRUE.equals(value) ? 1 : 0;
+    }
+
+    private Boolean integerToBoolean(Integer value) {
+        return value != null && value == 1;
+    }
+
+    private static final class MessagePushTypeDefinition {
+
+        private final String code;
+        private final String name;
+        private final boolean defaultPushEnabled;
+        private final boolean defaultSmsEnabled;
+
+        private MessagePushTypeDefinition(String code, String name, boolean defaultPushEnabled, boolean defaultSmsEnabled) {
+            this.code = code;
+            this.name = name;
+            this.defaultPushEnabled = defaultPushEnabled;
+            this.defaultSmsEnabled = defaultSmsEnabled;
+        }
+
+        private String getCode() {
+            return code;
+        }
+
+        private String getName() {
+            return name;
+        }
+
+        private boolean getDefaultPushEnabled() {
+            return defaultPushEnabled;
+        }
+
+        private boolean getDefaultSmsEnabled() {
+            return defaultSmsEnabled;
+        }
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/service/SystemBannerAppService.java b/backend/src/main/java/com/tcm/training/admin/system/service/SystemBannerAppService.java
new file mode 100644
index 0000000..9e1a758
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/service/SystemBannerAppService.java
@@ -0,0 +1,313 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.live.entity.HealthCourse;
+import com.tcm.training.admin.live.entity.LiveCourse;
+import com.tcm.training.admin.live.mapper.HealthCourseMapper;
+import com.tcm.training.admin.live.mapper.LiveCourseMapper;
+import com.tcm.training.admin.system.dto.SystemBannerBatchDeleteRequest;
+import com.tcm.training.admin.system.dto.SystemBannerPublishStatusRequest;
+import com.tcm.training.admin.system.dto.SystemBannerSaveRequest;
+import com.tcm.training.admin.system.entity.SystemBannerConfig;
+import com.tcm.training.admin.system.mapper.SystemBannerConfigMapper;
+import com.tcm.training.admin.system.vo.SystemBannerCourseOptionVO;
+import com.tcm.training.admin.system.vo.SystemBannerDetailVO;
+import com.tcm.training.admin.system.vo.SystemBannerListItemVO;
+import com.tcm.training.admin.system.vo.SystemBannerPublishStatusVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@Service
+public class SystemBannerAppService {
+
+    private final SystemBannerConfigMapper systemBannerConfigMapper;
+    private final HealthCourseMapper healthCourseMapper;
+    private final LiveCourseMapper liveCourseMapper;
+
+    public SystemBannerAppService(SystemBannerConfigMapper systemBannerConfigMapper,
+                                  HealthCourseMapper healthCourseMapper,
+                                  LiveCourseMapper liveCourseMapper) {
+        this.systemBannerConfigMapper = systemBannerConfigMapper;
+        this.healthCourseMapper = healthCourseMapper;
+        this.liveCourseMapper = liveCourseMapper;
+    }
+
+    public PageResponse<SystemBannerListItemVO> page(int page, int pageSize, String title, Integer status) {
+        LambdaQueryWrapper<SystemBannerConfig> qw = new LambdaQueryWrapper<SystemBannerConfig>()
+                .eq(SystemBannerConfig::getDeleted, 0)
+                .orderByDesc(SystemBannerConfig::getId);
+        String trimmedTitle = trimToNull(title);
+        if (trimmedTitle != null) {
+            qw.like(SystemBannerConfig::getTitle, trimmedTitle);
+        }
+
+        LocalDateTime now = LocalDateTime.now();
+        if (status != null) {
+            if (status == SystemBannerConfig.DISPLAY_STATUS_NOT_STARTED) {
+                qw.gt(SystemBannerConfig::getStartAt, now);
+            } else if (status == SystemBannerConfig.DISPLAY_STATUS_ONGOING) {
+                qw.le(SystemBannerConfig::getStartAt, now).ge(SystemBannerConfig::getEndAt, now);
+            } else if (status == SystemBannerConfig.DISPLAY_STATUS_ENDED) {
+                qw.lt(SystemBannerConfig::getEndAt, now);
+            } else {
+                throw new IllegalArgumentException("状态筛选不合法");
+            }
+        }
+
+        IPage<SystemBannerConfig> result = systemBannerConfigMapper.selectPage(new Page<SystemBannerConfig>(page, pageSize), qw);
+        List<SystemBannerListItemVO> list = new ArrayList<SystemBannerListItemVO>();
+        for (SystemBannerConfig item : result.getRecords()) {
+            list.add(toListItem(item));
+        }
+        return PageResponse.of(list, result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    public SystemBannerDetailVO detail(Long id) {
+        return toDetailVO(requireItem(id));
+    }
+
+    @Transactional
+    public SystemBannerDetailVO create(SystemBannerSaveRequest request) {
+        SystemBannerConfig item = new SystemBannerConfig();
+        fillEntity(item, request);
+        item.setDeleted(0);
+        systemBannerConfigMapper.insert(item);
+        return detail(item.getId());
+    }
+
+    @Transactional
+    public SystemBannerDetailVO update(Long id, SystemBannerSaveRequest request) {
+        SystemBannerConfig item = requireItem(id);
+        fillEntity(item, request);
+        systemBannerConfigMapper.updateById(item);
+        return detail(id);
+    }
+
+    @Transactional
+    public SystemBannerPublishStatusVO patchPublishStatus(Long id, SystemBannerPublishStatusRequest request) {
+        SystemBannerConfig item = requireItem(id);
+        item.setPublishEnabled(booleanToInteger(request.getPublishEnabled()));
+        systemBannerConfigMapper.updateById(item);
+        return new SystemBannerPublishStatusVO(id, request.getPublishEnabled());
+    }
+
+    @Transactional
+    public void delete(Long id) {
+        requireItem(id);
+        systemBannerConfigMapper.deleteById(id);
+    }
+
+    @Transactional
+    public void batchDelete(SystemBannerBatchDeleteRequest request) {
+        Set<Long> ids = new LinkedHashSet<Long>(request.getIds());
+        if (ids.isEmpty()) {
+            throw new IllegalArgumentException("请选择要删除的banner");
+        }
+        for (Long id : ids) {
+            requireItem(id);
+        }
+        systemBannerConfigMapper.deleteBatchIds(new ArrayList<Long>(ids));
+    }
+
+    public List<SystemBannerCourseOptionVO> healthCourseOptions(String keyword) {
+        List<HealthCourse> list = healthCourseMapper.selectList(new LambdaQueryWrapper<HealthCourse>()
+                .eq(HealthCourse::getDeleted, 0)
+                .like(keyword != null && !keyword.trim().isEmpty(), HealthCourse::getTitle, keyword == null ? null : keyword.trim())
+                .orderByAsc(HealthCourse::getSort)
+                .orderByDesc(HealthCourse::getId));
+        List<SystemBannerCourseOptionVO> options = new ArrayList<SystemBannerCourseOptionVO>();
+        for (HealthCourse item : list) {
+            options.add(new SystemBannerCourseOptionVO(item.getId(), item.getTitle()));
+        }
+        return options;
+    }
+
+    public List<SystemBannerCourseOptionVO> liveCourseOptions(String keyword) {
+        List<LiveCourse> list = liveCourseMapper.selectList(new LambdaQueryWrapper<LiveCourse>()
+                .eq(LiveCourse::getDeleted, 0)
+                .like(keyword != null && !keyword.trim().isEmpty(), LiveCourse::getTitle, keyword == null ? null : keyword.trim())
+                .orderByDesc(LiveCourse::getId));
+        List<SystemBannerCourseOptionVO> options = new ArrayList<SystemBannerCourseOptionVO>();
+        for (LiveCourse item : list) {
+            options.add(new SystemBannerCourseOptionVO(item.getId(), item.getTitle()));
+        }
+        return options;
+    }
+
+    private void fillEntity(SystemBannerConfig item, SystemBannerSaveRequest request) {
+        LocalDateTime startAt = request.getStartAt();
+        LocalDateTime endAt = request.getEndAt();
+        if (!endAt.isAfter(startAt)) {
+            throw new IllegalArgumentException("banner结束时间必须晚于开始时间");
+        }
+
+        String jumpType = normalizeJumpType(request.getJumpType());
+        item.setBannerImg(request.getBannerImg().trim());
+        item.setTitle(request.getTitle().trim());
+        item.setStartAt(startAt);
+        item.setEndAt(endAt);
+        item.setJumpType(jumpType);
+        item.setPublishEnabled(booleanToInteger(request.getPublishEnabled()));
+
+        if (SystemBannerConfig.JUMP_TYPE_HEALTH_COURSE.equals(jumpType)) {
+            HealthCourse course = requireHealthCourse(request.getTargetId());
+            item.setTargetId(course.getId());
+            item.setTargetName(course.getTitle());
+            item.setTargetUrl(null);
+            return;
+        }
+
+        if (SystemBannerConfig.JUMP_TYPE_LIVE_COURSE.equals(jumpType)) {
+            LiveCourse course = requireLiveCourse(request.getTargetId());
+            item.setTargetId(course.getId());
+            item.setTargetName(course.getTitle());
+            item.setTargetUrl(null);
+            return;
+        }
+
+        if (SystemBannerConfig.JUMP_TYPE_MEMBER.equals(jumpType)) {
+            item.setTargetId(null);
+            item.setTargetName("会员");
+            item.setTargetUrl(null);
+            return;
+        }
+
+        String targetUrl = trimToNull(request.getTargetUrl());
+        if (targetUrl == null) {
+            throw new IllegalArgumentException("请输入地址信息");
+        }
+        item.setTargetId(null);
+        item.setTargetName(null);
+        item.setTargetUrl(targetUrl);
+    }
+
+    private String normalizeJumpType(String jumpType) {
+        if (jumpType == null) {
+            throw new IllegalArgumentException("请选择跳转类型");
+        }
+        String normalized = jumpType.trim().toUpperCase();
+        if (SystemBannerConfig.JUMP_TYPE_HEALTH_COURSE.equals(normalized)
+                || SystemBannerConfig.JUMP_TYPE_LIVE_COURSE.equals(normalized)
+                || SystemBannerConfig.JUMP_TYPE_MEMBER.equals(normalized)
+                || SystemBannerConfig.JUMP_TYPE_THIRD_PARTY_URL.equals(normalized)) {
+            return normalized;
+        }
+        throw new IllegalArgumentException("跳转类型不合法");
+    }
+
+    private HealthCourse requireHealthCourse(Long targetId) {
+        if (targetId == null) {
+            throw new IllegalArgumentException("请选择养生课程");
+        }
+        HealthCourse course = healthCourseMapper.selectById(targetId);
+        if (course == null || defaultNumber(course.getDeleted()) == 1) {
+            throw new IllegalArgumentException("养生课程不存在");
+        }
+        return course;
+    }
+
+    private LiveCourse requireLiveCourse(Long targetId) {
+        if (targetId == null) {
+            throw new IllegalArgumentException("请选择直播课程");
+        }
+        LiveCourse course = liveCourseMapper.selectById(targetId);
+        if (course == null || defaultNumber(course.getDeleted()) == 1) {
+            throw new IllegalArgumentException("直播课程不存在");
+        }
+        return course;
+    }
+
+    private SystemBannerConfig requireItem(Long id) {
+        SystemBannerConfig item = systemBannerConfigMapper.selectOne(new LambdaQueryWrapper<SystemBannerConfig>()
+                .eq(SystemBannerConfig::getId, id)
+                .eq(SystemBannerConfig::getDeleted, 0)
+                .last("limit 1"));
+        if (item == null) {
+            throw new IllegalArgumentException("banner不存在");
+        }
+        return item;
+    }
+
+    private SystemBannerListItemVO toListItem(SystemBannerConfig item) {
+        SystemBannerListItemVO vo = new SystemBannerListItemVO();
+        vo.setId(item.getId());
+        vo.setBannerImg(item.getBannerImg());
+        vo.setTitle(item.getTitle());
+        vo.setStartAt(item.getStartAt());
+        vo.setEndAt(item.getEndAt());
+        vo.setStatus(resolveDisplayStatus(item.getStartAt(), item.getEndAt()));
+        vo.setPublishEnabled(integerToBoolean(item.getPublishEnabled()));
+        vo.setJumpType(item.getJumpType());
+        vo.setJumpTargetDisplay(resolveJumpTargetDisplay(item));
+        vo.setCreatedAt(item.getCreatedAt());
+        return vo;
+    }
+
+    private SystemBannerDetailVO toDetailVO(SystemBannerConfig item) {
+        SystemBannerDetailVO vo = new SystemBannerDetailVO();
+        vo.setId(item.getId());
+        vo.setBannerImg(item.getBannerImg());
+        vo.setTitle(item.getTitle());
+        vo.setStartAt(item.getStartAt());
+        vo.setEndAt(item.getEndAt());
+        vo.setJumpType(item.getJumpType());
+        vo.setTargetId(item.getTargetId());
+        vo.setTargetName(item.getTargetName());
+        vo.setTargetUrl(item.getTargetUrl());
+        vo.setPublishEnabled(integerToBoolean(item.getPublishEnabled()));
+        vo.setStatus(resolveDisplayStatus(item.getStartAt(), item.getEndAt()));
+        vo.setCreatedAt(item.getCreatedAt());
+        vo.setUpdatedAt(item.getUpdatedAt());
+        return vo;
+    }
+
+    private Integer resolveDisplayStatus(LocalDateTime startAt, LocalDateTime endAt) {
+        LocalDateTime now = LocalDateTime.now();
+        if (startAt != null && now.isBefore(startAt)) {
+            return SystemBannerConfig.DISPLAY_STATUS_NOT_STARTED;
+        }
+        if (endAt != null && now.isAfter(endAt)) {
+            return SystemBannerConfig.DISPLAY_STATUS_ENDED;
+        }
+        return SystemBannerConfig.DISPLAY_STATUS_ONGOING;
+    }
+
+    private String resolveJumpTargetDisplay(SystemBannerConfig item) {
+        if (SystemBannerConfig.JUMP_TYPE_MEMBER.equals(item.getJumpType())) {
+            return "会员";
+        }
+        if (SystemBannerConfig.JUMP_TYPE_THIRD_PARTY_URL.equals(item.getJumpType())) {
+            return item.getTargetUrl();
+        }
+        return item.getTargetName();
+    }
+
+    private Integer booleanToInteger(Boolean value) {
+        return Boolean.TRUE.equals(value) ? 1 : 0;
+    }
+
+    private Boolean integerToBoolean(Integer value) {
+        return value != null && value == 1;
+    }
+
+    private Integer defaultNumber(Integer value) {
+        return value == null ? 0 : value;
+    }
+
+    private String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/service/SystemSettingsAppService.java b/backend/src/main/java/com/tcm/training/admin/system/service/SystemSettingsAppService.java
new file mode 100644
index 0000000..fc1c45d
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/service/SystemSettingsAppService.java
@@ -0,0 +1,64 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tcm.training.admin.system.dto.SystemSettingsSaveRequest;
+import com.tcm.training.admin.system.entity.SystemSettings;
+import com.tcm.training.admin.system.mapper.SystemSettingsMapper;
+import com.tcm.training.admin.system.vo.SystemSettingsVO;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+public class SystemSettingsAppService {
+
+    private final SystemSettingsMapper systemSettingsMapper;
+
+    public SystemSettingsAppService(SystemSettingsMapper systemSettingsMapper) {
+        this.systemSettingsMapper = systemSettingsMapper;
+    }
+
+    public SystemSettingsVO current() {
+        SystemSettings config = systemSettingsMapper.selectOne(new LambdaQueryWrapper<SystemSettings>()
+                .eq(SystemSettings::getDeleted, 0)
+                .orderByDesc(SystemSettings::getId)
+                .last("limit 1"));
+        return toVO(config);
+    }
+
+    @Transactional
+    public SystemSettingsVO saveCurrent(SystemSettingsSaveRequest request) {
+        SystemSettings config = systemSettingsMapper.selectOne(new LambdaQueryWrapper<SystemSettings>()
+                .eq(SystemSettings::getDeleted, 0)
+                .orderByDesc(SystemSettings::getId)
+                .last("limit 1"));
+        if (config == null) {
+            config = new SystemSettings();
+            fillConfig(config, request);
+            systemSettingsMapper.insert(config);
+        } else {
+            fillConfig(config, request);
+            systemSettingsMapper.updateById(config);
+        }
+        return toVO(config);
+    }
+
+    private void fillConfig(SystemSettings config, SystemSettingsSaveRequest request) {
+        config.setServicePhone(request.getServicePhone().trim());
+        config.setServiceOnlineTime(request.getServiceOnlineTime().trim());
+        config.setBackupCycleDays(request.getBackupCycleDays());
+        config.setMemberExpireRemindDays(request.getMemberExpireRemindDays());
+    }
+
+    private SystemSettingsVO toVO(SystemSettings config) {
+        if (config == null) {
+            return null;
+        }
+        SystemSettingsVO vo = new SystemSettingsVO();
+        vo.setId(config.getId());
+        vo.setServicePhone(config.getServicePhone());
+        vo.setServiceOnlineTime(config.getServiceOnlineTime());
+        vo.setBackupCycleDays(config.getBackupCycleDays());
+        vo.setMemberExpireRemindDays(config.getMemberExpireRemindDays());
+        return vo;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/ActivityNoticeListItemVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/ActivityNoticeListItemVO.java
new file mode 100644
index 0000000..956ef2d
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/ActivityNoticeListItemVO.java
@@ -0,0 +1,17 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class ActivityNoticeListItemVO {
+
+    private Long id;
+    private String title;
+    private LocalDateTime createdAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionCheckVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionCheckVO.java
new file mode 100644
index 0000000..ec5421d
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionCheckVO.java
@@ -0,0 +1,18 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppVersionCheckVO {
+
+    private Boolean hasUpdate;
+    private String latestVersionNo;
+    private Integer latestVersionCode;
+    private Boolean forceUpdate;
+    private String updateNotice;
+    private String downloadUrl;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionDetailVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionDetailVO.java
new file mode 100644
index 0000000..d54fe2b
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionDetailVO.java
@@ -0,0 +1,24 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppVersionDetailVO {
+
+    private Long id;
+    private String versionNo;
+    private Integer versionCode;
+    private String androidFileUrl;
+    private String androidFileName;
+    private String iosFileUrl;
+    private String iosFileName;
+    private Boolean forceUpdate;
+    private String updateNotice;
+    private LocalDateTime createdAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionListItemVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionListItemVO.java
new file mode 100644
index 0000000..7c6d1e1
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/AppVersionListItemVO.java
@@ -0,0 +1,18 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppVersionListItemVO {
+
+    private Long id;
+    private String versionNo;
+    private String updateNotice;
+    private LocalDateTime createdAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/MessagePushSettingVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/MessagePushSettingVO.java
new file mode 100644
index 0000000..ca25afc
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/MessagePushSettingVO.java
@@ -0,0 +1,20 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class MessagePushSettingVO {
+
+    private Long id;
+    private String noticeTypeCode;
+    private String noticeTypeName;
+    private Boolean pushEnabled;
+    private Boolean smsEnabled;
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerCourseOptionVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerCourseOptionVO.java
new file mode 100644
index 0000000..52597bc
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerCourseOptionVO.java
@@ -0,0 +1,19 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerCourseOptionVO {
+
+    private Long id;
+    private String title;
+
+    public SystemBannerCourseOptionVO(Long id, String title) {
+        this.id = id;
+        this.title = title;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerDetailVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerDetailVO.java
new file mode 100644
index 0000000..10469ac
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerDetailVO.java
@@ -0,0 +1,27 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerDetailVO {
+
+    private Long id;
+    private String bannerImg;
+    private String title;
+    private LocalDateTime startAt;
+    private LocalDateTime endAt;
+    private String jumpType;
+    private Long targetId;
+    private String targetName;
+    private String targetUrl;
+    private Boolean publishEnabled;
+    private Integer status;
+    private LocalDateTime createdAt;
+    private LocalDateTime updatedAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerListItemVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerListItemVO.java
new file mode 100644
index 0000000..b027e18
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerListItemVO.java
@@ -0,0 +1,24 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerListItemVO {
+
+    private Long id;
+    private String bannerImg;
+    private String title;
+    private LocalDateTime startAt;
+    private LocalDateTime endAt;
+    private Integer status;
+    private Boolean publishEnabled;
+    private String jumpType;
+    private String jumpTargetDisplay;
+    private LocalDateTime createdAt;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerPublishStatusVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerPublishStatusVO.java
new file mode 100644
index 0000000..d1ad2ec
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemBannerPublishStatusVO.java
@@ -0,0 +1,19 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemBannerPublishStatusVO {
+
+    private Long id;
+    private Boolean publishEnabled;
+
+    public SystemBannerPublishStatusVO(Long id, Boolean publishEnabled) {
+        this.id = id;
+        this.publishEnabled = publishEnabled;
+    }
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/system/vo/SystemSettingsVO.java b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemSettingsVO.java
new file mode 100644
index 0000000..70e20af
--- /dev/null
+++ b/backend/src/main/java/com/tcm/training/admin/system/vo/SystemSettingsVO.java
@@ -0,0 +1,17 @@
+package com.tcm.training.admin.system.vo;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Getter
+@Setter
+@NoArgsConstructor
+public class SystemSettingsVO {
+
+    private Long id;
+    private String servicePhone;
+    private String serviceOnlineTime;
+    private Integer backupCycleDays;
+    private Integer memberExpireRemindDays;
+}
diff --git a/backend/src/main/java/com/tcm/training/admin/upload/UploadController.java b/backend/src/main/java/com/tcm/training/admin/upload/UploadController.java
index fdf149b..2c37f64 100644
--- a/backend/src/main/java/com/tcm/training/admin/upload/UploadController.java
+++ b/backend/src/main/java/com/tcm/training/admin/upload/UploadController.java
@@ -7,6 +7,7 @@
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
+import org.springframework.util.StringUtils;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -19,12 +20,24 @@
 
     private static final long MAX_SIZE = 5 * 1024 * 1024L; // 5MB
     private static final long VIDEO_MAX_SIZE = 200 * 1024 * 1024L; // 200MB
+    private static final long APP_PACKAGE_MAX_SIZE = 500 * 1024 * 1024L; // 500MB
     private static final Set<String> ALLOWED_TYPES = new HashSet<>(
             Arrays.asList("image/jpeg", "image/png", "image/gif", "image/webp")
     );
     private static final Set<String> ALLOWED_VIDEO_TYPES = new HashSet<>(
             Arrays.asList("video/mp4", "video/quicktime", "video/x-msvideo", "video/x-matroska", "video/webm")
     );
+    private static final Set<String> ALLOWED_APP_PACKAGE_TYPES = new HashSet<>(
+            Arrays.asList(
+                    "application/vnd.android.package-archive",
+                    "application/octet-stream",
+                    "application/x-itunes-ipa",
+                    "application/zip",
+                    "application/x-zip-compressed"
+            )
+    );
+    private static final String PLATFORM_ANDROID = "ANDROID";
+    private static final String PLATFORM_IOS = "IOS";
 
     private final ObsUploadService obsUploadService;
     private final UploadProgressService uploadProgressService;
@@ -52,6 +65,16 @@
         return ApiResponse.ok(storeFile(file, VIDEO_MAX_SIZE, ALLOWED_VIDEO_TYPES, "仅支持 MP4、MOV、AVI、MKV、WebM 格式", "video", uploadTaskId));
     }
 
+    @PostMapping("/upload/app-package")
+    public ApiResponse<UploadResult> uploadAppPackage(
+            @RequestParam("file") MultipartFile file,
+            @RequestParam("platform") String platform) throws IOException {
+        String normalizedPlatform = normalizePlatform(platform);
+        validateAppPackage(file, normalizedPlatform);
+        UploadFileResult uploadResult = obsUploadService.upload(file, "app-package/" + normalizedPlatform.toLowerCase(), null);
+        return ApiResponse.ok(new UploadResult(uploadResult.getUrl(), uploadResult.getFileName()));
+    }
+
     @GetMapping("/upload/video/progress")
     public ApiResponse<UploadProgressInfo> uploadVideoProgress(
             @RequestParam("taskId") String taskId) {
@@ -64,12 +87,7 @@
                                    String typeMessage,
                                    String bizPath,
                                    String uploadTaskId) throws IOException {
-        if (file == null || file.isEmpty()) {
-            throw new IllegalArgumentException("请选择要上传的文件");
-        }
-        if (file.getSize() > maxSize) {
-            throw new IllegalArgumentException("文件大小超出限制");
-        }
+        validateFileSize(file, maxSize);
         String contentType = file.getContentType();
         if (contentType == null || !allowedTypes.contains(contentType)) {
             throw new IllegalArgumentException(typeMessage);
@@ -78,6 +96,45 @@
         return new UploadResult(uploadResult.getUrl(), uploadResult.getFileName());
     }
 
+    private void validateFileSize(MultipartFile file, long maxSize) {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("请选择要上传的文件");
+        }
+        if (file.getSize() > maxSize) {
+            throw new IllegalArgumentException("文件大小超出限制");
+        }
+    }
+
+    private void validateAppPackage(MultipartFile file, String platform) {
+        validateFileSize(file, APP_PACKAGE_MAX_SIZE);
+        String originalName = file.getOriginalFilename();
+        String extension = "";
+        if (StringUtils.hasText(originalName) && originalName.contains(".")) {
+            extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase();
+        }
+        if (PLATFORM_ANDROID.equals(platform) && !"apk".equals(extension)) {
+            throw new IllegalArgumentException("安卓安装包仅支持 APK 格式");
+        }
+        if (PLATFORM_IOS.equals(platform) && !"ipa".equals(extension)) {
+            throw new IllegalArgumentException("iOS 安装包仅支持 IPA 格式");
+        }
+        String contentType = file.getContentType();
+        if (StringUtils.hasText(contentType) && !ALLOWED_APP_PACKAGE_TYPES.contains(contentType)) {
+            throw new IllegalArgumentException("安装包格式不合法");
+        }
+    }
+
+    private String normalizePlatform(String platform) {
+        if (!StringUtils.hasText(platform)) {
+            throw new IllegalArgumentException("平台类型不能为空");
+        }
+        String normalized = platform.trim().toUpperCase();
+        if (PLATFORM_ANDROID.equals(normalized) || PLATFORM_IOS.equals(normalized)) {
+            return normalized;
+        }
+        throw new IllegalArgumentException("平台类型不合法");
+    }
+
     public static class UploadResult {
         private final String url;
         private final String fileName;
diff --git a/backend/src/test/java/com/tcm/training/admin/system/service/ActivityNoticeAppServiceTest.java b/backend/src/test/java/com/tcm/training/admin/system/service/ActivityNoticeAppServiceTest.java
new file mode 100644
index 0000000..aa0fec2
--- /dev/null
+++ b/backend/src/test/java/com/tcm/training/admin/system/service/ActivityNoticeAppServiceTest.java
@@ -0,0 +1,84 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.tcm.training.admin.system.dto.ActivityNoticeCreateRequest;
+import com.tcm.training.admin.system.entity.ActivityNotice;
+import com.tcm.training.admin.system.mapper.ActivityNoticeMapper;
+import com.tcm.training.admin.system.vo.ActivityNoticeListItemVO;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ActivityNoticeAppServiceTest {
+
+    @BeforeAll
+    static void initLambdaCache() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, ActivityNotice.class);
+    }
+
+    @Mock
+    private ActivityNoticeMapper activityNoticeMapper;
+
+    @InjectMocks
+    private ActivityNoticeAppService activityNoticeAppService;
+
+    @Captor
+    private ArgumentCaptor<ActivityNotice> activityNoticeCaptor;
+
+    @Test
+    void createShouldTrimTitleAndPersistContent() {
+        doAnswer(invocation -> {
+            ActivityNotice item = invocation.getArgument(0);
+            item.setId(1L);
+            return 1;
+        }).when(activityNoticeMapper).insert(any(ActivityNotice.class));
+
+        ActivityNotice persisted = new ActivityNotice();
+        persisted.setId(1L);
+        persisted.setTitle("春季活动");
+        persisted.setContentHtml("<p>活动内容</p>");
+        persisted.setCreatedAt(LocalDateTime.of(2026, 3, 26, 18, 10, 0));
+        when(activityNoticeMapper.selectOne(any())).thenReturn(persisted);
+
+        ActivityNoticeCreateRequest request = new ActivityNoticeCreateRequest();
+        request.setTitle("  春季活动  ");
+        request.setContentHtml("<p>活动内容</p>");
+
+        ActivityNoticeListItemVO result = activityNoticeAppService.create(request);
+
+        verify(activityNoticeMapper).insert(activityNoticeCaptor.capture());
+        assertEquals("春季活动", activityNoticeCaptor.getValue().getTitle());
+        assertEquals("<p>活动内容</p>", activityNoticeCaptor.getValue().getContentHtml());
+        assertEquals("春季活动", result.getTitle());
+    }
+
+    @Test
+    void createShouldRejectBlankRichTextContent() {
+        ActivityNoticeCreateRequest request = new ActivityNoticeCreateRequest();
+        request.setTitle("活动标题");
+        request.setContentHtml("<p><br/></p>");
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> activityNoticeAppService.create(request));
+
+        assertEquals("请输入通知内容", exception.getMessage());
+    }
+}
diff --git a/backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java b/backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
new file mode 100644
index 0000000..5e917c9
--- /dev/null
+++ b/backend/src/test/java/com/tcm/training/admin/system/service/AppVersionSettingsAppServiceTest.java
@@ -0,0 +1,105 @@
+package com.tcm.training.admin.system.service;
+
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.tcm.training.admin.system.dto.AppVersionCreateRequest;
+import com.tcm.training.admin.system.entity.AppVersionConfig;
+import com.tcm.training.admin.system.mapper.AppVersionConfigMapper;
+import com.tcm.training.admin.system.vo.AppVersionCheckVO;
+import com.tcm.training.admin.system.vo.AppVersionDetailVO;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AppVersionSettingsAppServiceTest {
+
+    @BeforeAll
+    static void initLambdaCache() {
+        MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
+        TableInfoHelper.initTableInfo(assistant, AppVersionConfig.class);
+    }
+
+    @Mock
+    private AppVersionConfigMapper appVersionConfigMapper;
+
+    @InjectMocks
+    private AppVersionSettingsAppService appVersionSettingsAppService;
+
+    @Captor
+    private ArgumentCaptor<AppVersionConfig> appVersionCaptor;
+
+    @Test
+    void createShouldIncrementVersionCodeFromLatestRecord() {
+        AppVersionConfig latest = new AppVersionConfig();
+        latest.setId(9L);
+        latest.setVersionCode(3);
+        when(appVersionConfigMapper.selectOne(any())).thenReturn(latest);
+
+        AppVersionCreateRequest request = new AppVersionCreateRequest();
+        request.setVersionNo("1.0.3");
+        request.setAndroidFileUrl("https://cdn.test/app-android.apk");
+        request.setAndroidFileName("app-android.apk");
+        request.setIosFileUrl("https://cdn.test/app-ios.ipa");
+        request.setIosFileName("app-ios.ipa");
+        request.setForceUpdate(Boolean.TRUE);
+        request.setUpdateNotice("修复已知问题");
+
+        AppVersionDetailVO result = appVersionSettingsAppService.create(request);
+
+        verify(appVersionConfigMapper).insert(appVersionCaptor.capture());
+        assertEquals(Integer.valueOf(4), appVersionCaptor.getValue().getVersionCode());
+        assertEquals(Integer.valueOf(4), result.getVersionCode());
+        assertTrue(result.getForceUpdate());
+    }
+
+    @Test
+    void checkUpdateShouldReturnLatestDownloadForAndroid() {
+        AppVersionConfig latest = new AppVersionConfig();
+        latest.setVersionNo("1.0.5");
+        latest.setVersionCode(5);
+        latest.setAndroidFileUrl("https://cdn.test/app-android.apk");
+        latest.setIosFileUrl("https://cdn.test/app-ios.ipa");
+        latest.setForceUpdate(1);
+        latest.setUpdateNotice("新增版本内容");
+        when(appVersionConfigMapper.selectOne(any())).thenReturn(latest);
+
+        AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("android", 3);
+
+        assertTrue(result.getHasUpdate());
+        assertEquals("1.0.5", result.getLatestVersionNo());
+        assertEquals(Integer.valueOf(5), result.getLatestVersionCode());
+        assertEquals("https://cdn.test/app-android.apk", result.getDownloadUrl());
+        assertTrue(result.getForceUpdate());
+    }
+
+    @Test
+    void checkUpdateShouldReturnFalseWhenCurrentVersionIsLatest() {
+        AppVersionConfig latest = new AppVersionConfig();
+        latest.setVersionNo("1.0.5");
+        latest.setVersionCode(5);
+        latest.setAndroidFileUrl("https://cdn.test/app-android.apk");
+        latest.setIosFileUrl("https://cdn.test/app-ios.ipa");
+        latest.setForceUpdate(0);
+        latest.setUpdateNotice("新增版本内容");
+        when(appVersionConfigMapper.selectOne(any())).thenReturn(latest);
+
+        AppVersionCheckVO result = appVersionSettingsAppService.checkUpdate("IOS", 5);
+
+        assertFalse(result.getHasUpdate());
+        assertEquals("https://cdn.test/app-ios.ipa", result.getDownloadUrl());
+    }
+}
diff --git a/tcm-training-admin/config/routes.ts b/tcm-training-admin/config/routes.ts
index bab682f..29e5c71 100644
--- a/tcm-training-admin/config/routes.ts
+++ b/tcm-training-admin/config/routes.ts
@@ -71,7 +71,38 @@
         routes: [
           {
             path: '/system/settings',
-            redirect: '/system/settings/points',
+            hideInMenu: true,
+            component: './system/settings-redirect',
+          },
+          {
+            name: '系统设置',
+            path: '/system/settings/basic',
+            access: 'system:basic-settings:view',
+            component: './system/basic-settings',
+          },
+          {
+            name: 'APP版本设置',
+            path: '/system/settings/app-version',
+            access: 'system:app-version:view',
+            component: './system/app-version-settings',
+          },
+          {
+            name: 'Banner配置',
+            path: '/system/settings/banner',
+            access: 'system:banner:view',
+            component: './system/banner-settings',
+          },
+          {
+            name: '活动通知',
+            path: '/system/settings/activity-notice',
+            access: 'system:activity-notice:view',
+            component: './system/activity-notice',
+          },
+          {
+            name: '消息推送配置',
+            path: '/system/settings/message-push',
+            access: 'system:message-push:view',
+            component: './system/message-push-settings',
           },
           {
             name: '积分设置',
diff --git a/tcm-training-admin/src/access.js b/tcm-training-admin/src/access.js
index 244b414..0381ae8 100644
--- a/tcm-training-admin/src/access.js
+++ b/tcm-training-admin/src/access.js
@@ -14,6 +14,11 @@
     canViewSystemSecurity:
       !!permission?.['system:account:view'] ||
       !!permission?.['system:role:view'] ||
+      !!permission?.['system:basic-settings:view'] ||
+      !!permission?.['system:app-version:view'] ||
+      !!permission?.['system:banner:view'] ||
+      !!permission?.['system:activity-notice:view'] ||
+      !!permission?.['system:message-push:view'] ||
       !!permission?.['system:points-settings:view'],
     canViewMemberModule: canViewMemberUserGroup || canViewMemberVipGroup,
     canViewMemberUserGroup,
diff --git a/tcm-training-admin/src/pages/system/activity-notice/index.tsx b/tcm-training-admin/src/pages/system/activity-notice/index.tsx
new file mode 100644
index 0000000..9793829
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/activity-notice/index.tsx
@@ -0,0 +1,287 @@
+import BraftEditorCom from '@/components/BraftEditor';
+import { PlusOutlined } from '@ant-design/icons';
+import { history, useAccess } from '@umijs/max';
+import { Button, Form, Input, Modal, Pagination, Table, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import React, { useEffect, useState } from 'react';
+import { createActivityNotice, deleteActivityNotice, getActivityNoticePage } from './service';
+import './style.less';
+
+type SearchValues = {
+  title?: string;
+};
+
+type ActivityNoticeItem = {
+  id: number;
+  title: string;
+  createdAt?: string;
+};
+
+type ActivityNoticeFormValues = {
+  title?: string;
+  contentHtml?: string;
+};
+
+const trimSearchValues = (values: SearchValues) => ({
+  title: values.title?.trim() || undefined,
+});
+
+const isBlankRichText = (html?: string) => {
+  const value = (html || '').trim();
+  if (!value) {
+    return true;
+  }
+  const normalized = value
+    .replace(/<br\s*\/?>/gi, '')
+    .replace(/<\/p>/gi, '')
+    .replace(/&nbsp;/gi, '')
+    .replace(/&#160;/gi, '')
+    .replace(/<[^>]+>/g, '')
+    .trim();
+  if (normalized) {
+    return false;
+  }
+  const lowerCaseValue = value.toLowerCase();
+  return !lowerCaseValue.includes('<img') && !lowerCaseValue.includes('<video');
+};
+
+const ActivityNoticePage: React.FC = () => {
+  const access = useAccess();
+  const [searchForm] = Form.useForm<SearchValues>();
+  const [form] = Form.useForm<ActivityNoticeFormValues>();
+  const [list, setList] = useState<ActivityNoticeItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [modalOpen, setModalOpen] = useState(false);
+  const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
+
+  const canView = access['system:activity-notice:view'];
+  const canAdd = access['system:activity-notice:add'];
+  const canDelete = access['system:activity-notice:delete'];
+
+  const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => {
+    setLoading(true);
+    try {
+      const res = await getActivityNoticePage({
+        page,
+        pageSize,
+        ...trimSearchValues(searchForm.getFieldsValue()),
+      });
+      setList(res.data?.list || []);
+      setPagination({
+        page: Number(res.data?.page || page),
+        pageSize: Number(res.data?.pageSize || pageSize),
+        total: Number(res.data?.total || 0),
+      });
+    } catch (error) {
+      message.error((error as Error)?.message || '活动通知列表加载失败');
+      setList([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!canView) {
+      history.replace('/Welcome');
+      return;
+    }
+    loadList(1, 10);
+  }, [canView]);
+
+  const handleCloseModal = () => {
+    if (submitting) {
+      return;
+    }
+    setModalOpen(false);
+    form.resetFields();
+  };
+
+  const handleCreate = async () => {
+    const values = await form.validateFields();
+    if (isBlankRichText(values.contentHtml)) {
+      form.setFields([
+        {
+          name: 'contentHtml',
+          errors: ['请输入通知内容'],
+        },
+      ]);
+      return;
+    }
+    setSubmitting(true);
+    try {
+      await createActivityNotice({
+        title: values.title?.trim(),
+        contentHtml: values.contentHtml?.trim(),
+      });
+      message.success('新建成功');
+      setModalOpen(false);
+      form.resetFields();
+      await loadList(1, pagination.pageSize);
+    } catch (error) {
+      message.error((error as Error)?.message || '新建失败,请稍后重试');
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const handleDelete = (record: ActivityNoticeItem) => {
+    Modal.confirm({
+      title: '确定要删除该活动通知吗?',
+      okText: '删除',
+      cancelText: '取消',
+      onOk: async () => {
+        try {
+          await deleteActivityNotice(record.id);
+          message.success('删除成功');
+          const nextPage =
+            list.length === 1 && pagination.page > 1 ? pagination.page - 1 : pagination.page;
+          await loadList(nextPage, pagination.pageSize);
+        } catch (error) {
+          message.error((error as Error)?.message || '删除失败,请稍后重试');
+          throw error;
+        }
+      },
+    });
+  };
+
+  const columns: ColumnsType<ActivityNoticeItem> = [
+    {
+      title: '序号',
+      width: 80,
+      render: (_, __, index) => (pagination.page - 1) * pagination.pageSize + index + 1,
+    },
+    {
+      title: '通知标题',
+      dataIndex: 'title',
+      ellipsis: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      width: 180,
+    },
+    {
+      title: '操作',
+      width: 120,
+      render: (_, record) =>
+        canDelete ? (
+          <Button type="link" size="small" onClick={() => handleDelete(record)}>
+            删除
+          </Button>
+        ) : null,
+    },
+  ];
+
+  return (
+    <div className="activity-notice-page">
+      <div className="activity-notice-card">
+        <div className="activity-notice-card__title">活动通知</div>
+        <div className="activity-notice-search-card">
+          <Form form={searchForm} layout="horizontal" className="activity-notice-search-form">
+            <Form.Item label="通知标题" name="title">
+              <Input placeholder="请输入" allowClear />
+            </Form.Item>
+            <div className="activity-notice-search-form__actions">
+              <Button
+                onClick={async () => {
+                  searchForm.resetFields();
+                  await loadList(1, pagination.pageSize);
+                }}
+              >
+                重置
+              </Button>
+              <Button type="primary" onClick={() => loadList(1, pagination.pageSize)}>
+                搜索
+              </Button>
+            </div>
+          </Form>
+        </div>
+
+        <div className="activity-notice-toolbar">
+          {canAdd ? (
+            <Button type="primary" icon={<PlusOutlined />} onClick={() => setModalOpen(true)}>
+              新建
+            </Button>
+          ) : null}
+        </div>
+
+        <div className="activity-notice-table-card">
+          <Table<ActivityNoticeItem>
+            rowKey="id"
+            columns={columns}
+            dataSource={list}
+            loading={loading}
+            pagination={false}
+            scroll={{ x: 900 }}
+          />
+          <div className="activity-notice-pagination">
+            <Pagination
+              current={pagination.page}
+              pageSize={pagination.pageSize}
+              total={pagination.total}
+              showSizeChanger
+              showQuickJumper
+              showTotal={(total) => `共 ${total} 条`}
+              onChange={(page, pageSize) => loadList(page, pageSize)}
+            />
+          </div>
+        </div>
+      </div>
+
+      <Modal
+        open={modalOpen}
+        className="activity-notice-modal"
+        destroyOnClose
+        centered
+        width={820}
+        title="新建活动"
+        onCancel={handleCloseModal}
+        footer={[
+          <Button key="cancel" onClick={handleCloseModal} disabled={submitting}>
+            取消
+          </Button>,
+          <Button key="submit" type="primary" loading={submitting} onClick={handleCreate}>
+            确定
+          </Button>,
+        ]}
+      >
+        <Form form={form} layout="horizontal" className="activity-notice-form" labelCol={{ span: 4 }}>
+          <Form.Item
+            label="通知标题"
+            name="title"
+            rules={[
+              { required: true, message: '请输入通知标题' },
+              { max: 15, message: '请输入15个字以内' },
+            ]}
+          >
+            <Input placeholder="请输入15个字以内" maxLength={15} />
+          </Form.Item>
+          <Form.Item
+            label="通知内容"
+            name="contentHtml"
+            rules={[
+              { required: true, message: '请输入通知内容' },
+              {
+                validator: async (_, value) => {
+                  if (isBlankRichText(value)) {
+                    throw new Error('请输入通知内容');
+                  }
+                },
+              },
+            ]}
+          >
+            <div className="activity-notice-form__editor">
+              <BraftEditorCom
+                data={form.getFieldValue('contentHtml') || ''}
+                resultCallback={(html: string) => form.setFieldsValue({ contentHtml: html })}
+              />
+            </div>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default ActivityNoticePage;
diff --git a/tcm-training-admin/src/pages/system/activity-notice/service.ts b/tcm-training-admin/src/pages/system/activity-notice/service.ts
new file mode 100644
index 0000000..71f65c9
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/activity-notice/service.ts
@@ -0,0 +1,21 @@
+import { request } from '@umijs/max';
+
+export async function getActivityNoticePage(params?: Record<string, any>) {
+  return request('/activity-notices', {
+    method: 'GET',
+    params,
+  });
+}
+
+export async function createActivityNotice(data: Record<string, any>) {
+  return request('/activity-notices', {
+    method: 'POST',
+    data,
+  });
+}
+
+export async function deleteActivityNotice(id: number | string) {
+  return request(`/activity-notices/${id}`, {
+    method: 'DELETE',
+  });
+}
diff --git a/tcm-training-admin/src/pages/system/activity-notice/style.less b/tcm-training-admin/src/pages/system/activity-notice/style.less
new file mode 100644
index 0000000..ac3fcc7
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/activity-notice/style.less
@@ -0,0 +1,123 @@
+.activity-notice-page {
+  min-height: calc(100vh - 48px);
+  padding: 18px 20px 24px;
+  background: #f2f5f9;
+}
+
+.activity-notice-card {
+  min-height: 700px;
+  padding: 8px 14px 18px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.activity-notice-card__title {
+  margin: 0 0 22px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.activity-notice-search-card {
+  margin-bottom: 20px;
+}
+
+.activity-notice-search-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0 18px;
+  align-items: center;
+}
+
+.activity-notice-search-form .ant-form-item {
+  margin-bottom: 0;
+}
+
+.activity-notice-search-form .ant-input {
+  width: 170px;
+}
+
+.activity-notice-search-form__actions {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.activity-notice-toolbar {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 14px;
+}
+
+.activity-notice-table-card {
+  overflow: hidden;
+  border: 1px solid #e6ebf2;
+  border-radius: 4px;
+}
+
+.activity-notice-table-card .ant-table-thead > tr > th {
+  color: #303133;
+  font-weight: 500;
+  background: #f5f7fa;
+}
+
+.activity-notice-table-card .ant-table-tbody > tr > td {
+  color: #606266;
+  vertical-align: middle;
+}
+
+.activity-notice-pagination {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 18px;
+  background: #fff;
+}
+
+.activity-notice-modal .ant-modal-content {
+  padding: 18px 24px 20px;
+  border-radius: 8px;
+}
+
+.activity-notice-modal .ant-modal-title {
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.activity-notice-modal .ant-modal-body {
+  padding-top: 12px;
+}
+
+.activity-notice-form .ant-form-item {
+  margin-bottom: 18px;
+}
+
+.activity-notice-form .ant-form-item-label > label {
+  color: #303133;
+}
+
+.activity-notice-form__editor {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+
+@media (max-width: 900px) {
+  .activity-notice-page {
+    padding: 12px;
+  }
+
+  .activity-notice-card {
+    min-height: auto;
+    padding: 16px;
+  }
+
+  .activity-notice-search-form {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .activity-notice-search-form .ant-input {
+    width: 100%;
+  }
+}
diff --git a/tcm-training-admin/src/pages/system/app-version-settings/index.tsx b/tcm-training-admin/src/pages/system/app-version-settings/index.tsx
new file mode 100644
index 0000000..17eb4a5
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/app-version-settings/index.tsx
@@ -0,0 +1,392 @@
+import {
+  createAppVersion,
+  getAppVersionDetail,
+  getAppVersionPage,
+  uploadAppPackage,
+} from './service';
+import { UploadOutlined } from '@ant-design/icons';
+import { history, useAccess } from '@umijs/max';
+import { Button, Form, Input, Modal, Pagination, Spin, Switch, Table, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import React, { useEffect, useState } from 'react';
+import './style.less';
+
+type AppVersionItem = {
+  createdAt: string;
+  id: number;
+  updateNotice: string;
+  versionNo: string;
+};
+
+type AppVersionDetail = {
+  androidFileName?: string;
+  androidFileUrl?: string;
+  forceUpdate?: boolean;
+  id: number;
+  iosFileName?: string;
+  iosFileUrl?: string;
+  updateNotice?: string;
+  versionCode?: number;
+  versionNo?: string;
+};
+
+type AppVersionFormValues = {
+  androidFileName?: string;
+  androidFileUrl?: string;
+  forceUpdate?: boolean;
+  iosFileName?: string;
+  iosFileUrl?: string;
+  updateNotice?: string;
+  versionNo?: string;
+};
+
+type ModalMode = 'create' | 'detail';
+
+const defaultValues: AppVersionFormValues = {
+  versionNo: '',
+  androidFileUrl: '',
+  androidFileName: '',
+  iosFileUrl: '',
+  iosFileName: '',
+  forceUpdate: false,
+  updateNotice: '',
+};
+
+const AppVersionSettingsPage: React.FC = () => {
+  const access = useAccess();
+  const [form] = Form.useForm<AppVersionFormValues>();
+  const androidFileName = Form.useWatch('androidFileName', form);
+  const iosFileName = Form.useWatch('iosFileName', form);
+  const [pageLoading, setPageLoading] = useState(false);
+  const [modalLoading, setModalLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [list, setList] = useState<AppVersionItem[]>([]);
+  const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
+  const [modalOpen, setModalOpen] = useState(false);
+  const [modalMode, setModalMode] = useState<ModalMode>('create');
+  const [uploadingAndroid, setUploadingAndroid] = useState(false);
+  const [uploadingIos, setUploadingIos] = useState(false);
+
+  const canView = access['system:app-version:view'];
+  const canAdd = access['system:app-version:add'];
+  const canDetail = access['system:app-version:detail'];
+  const isDetailMode = modalMode === 'detail';
+
+  const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => {
+    setPageLoading(true);
+    try {
+      const res = await getAppVersionPage({ page, pageSize });
+      setList(res.data?.list || []);
+      setPagination({
+        page: Number(res.data?.page || page),
+        pageSize: Number(res.data?.pageSize || pageSize),
+        total: Number(res.data?.total || 0),
+      });
+    } catch (error) {
+      message.error((error as Error)?.message || 'APP版本列表加载失败');
+    } finally {
+      setPageLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!canView) {
+      history.replace('/Welcome');
+      return;
+    }
+    loadList(1, 10);
+  }, [canView]);
+
+  const openCreateModal = () => {
+    setModalMode('create');
+    setModalLoading(false);
+    form.resetFields();
+    form.setFieldsValue(defaultValues);
+    setModalOpen(true);
+  };
+
+  const openDetailModal = async (record: AppVersionItem) => {
+    setModalMode('detail');
+    form.resetFields();
+    setModalOpen(true);
+    setModalLoading(true);
+    try {
+      const res = await getAppVersionDetail(record.id);
+      const detail: AppVersionDetail = res.data;
+      form.setFieldsValue({
+        versionNo: detail.versionNo || '',
+        androidFileUrl: detail.androidFileUrl || '',
+        androidFileName: detail.androidFileName || '',
+        iosFileUrl: detail.iosFileUrl || '',
+        iosFileName: detail.iosFileName || '',
+        forceUpdate: detail.forceUpdate || false,
+        updateNotice: detail.updateNotice || '',
+      });
+    } catch (error) {
+      message.error((error as Error)?.message || 'APP版本详情加载失败');
+      setModalOpen(false);
+    } finally {
+      setModalLoading(false);
+    }
+  };
+
+  const handleCloseModal = () => {
+    if (submitting || uploadingAndroid || uploadingIos) {
+      return;
+    }
+    setModalOpen(false);
+    setModalLoading(false);
+    form.resetFields();
+  };
+
+  const handlePackageUpload = async (file: File, platform: 'ANDROID' | 'IOS') => {
+    const setUploading = platform === 'ANDROID' ? setUploadingAndroid : setUploadingIos;
+    const urlField = platform === 'ANDROID' ? 'androidFileUrl' : 'iosFileUrl';
+    const nameField = platform === 'ANDROID' ? 'androidFileName' : 'iosFileName';
+    setUploading(true);
+    try {
+      const res = await uploadAppPackage(file, platform);
+      const url = res.data?.url;
+      if (!url) {
+        throw new Error('上传失败');
+      }
+      form.setFieldsValue({
+        [urlField]: url,
+        [nameField]: res.data?.fileName || file.name,
+      });
+      message.success(platform === 'ANDROID' ? '安卓安装包上传成功' : 'iOS安装包上传成功');
+    } catch (error) {
+      message.error((error as Error)?.message || '安装包上传失败');
+    } finally {
+      setUploading(false);
+    }
+  };
+
+  const handleSubmit = async () => {
+    if (isDetailMode) {
+      handleCloseModal();
+      return;
+    }
+    const values = await form.validateFields();
+    setSubmitting(true);
+    try {
+      await createAppVersion({
+        versionNo: values.versionNo?.trim(),
+        androidFileUrl: values.androidFileUrl?.trim(),
+        androidFileName: values.androidFileName?.trim(),
+        iosFileUrl: values.iosFileUrl?.trim(),
+        iosFileName: values.iosFileName?.trim(),
+        forceUpdate: !!values.forceUpdate,
+        updateNotice: values.updateNotice?.trim(),
+      });
+      message.success('新增成功');
+      setModalOpen(false);
+      form.resetFields();
+      await loadList(1, pagination.pageSize);
+    } catch (error) {
+      message.error((error as Error)?.message || '新增失败,请稍后重试');
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const columns: ColumnsType<AppVersionItem> = [
+    {
+      title: '序号',
+      width: 72,
+      render: (_, __, index) => (pagination.page - 1) * pagination.pageSize + index + 1,
+    },
+    {
+      title: '版本号',
+      dataIndex: 'versionNo',
+      width: 150,
+    },
+    {
+      title: '更新提示',
+      dataIndex: 'updateNotice',
+      ellipsis: true,
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      width: 180,
+    },
+    {
+      title: '操作',
+      width: 120,
+      render: (_, record) =>
+        canDetail ? (
+          <button
+            type="button"
+            className="app-version-settings-table__link"
+            onClick={() => openDetailModal(record)}
+          >
+            查看
+          </button>
+        ) : null,
+    },
+  ];
+
+  return (
+    <div className="app-version-settings-page">
+      <div className="app-version-settings-panel">
+        <div className="app-version-settings-panel__title">APP版本设置</div>
+        <div className="app-version-settings-toolbar">
+          {canAdd ? (
+            <Button type="primary" onClick={openCreateModal}>
+              新增版本号
+            </Button>
+          ) : null}
+        </div>
+        <div className="app-version-settings-table-card">
+          <Table<AppVersionItem>
+            rowKey="id"
+            columns={columns}
+            dataSource={list}
+            loading={pageLoading}
+            pagination={false}
+            scroll={{ x: 920 }}
+          />
+          {pagination.total > pagination.pageSize ? (
+            <div className="app-version-settings-pagination">
+              <Pagination
+                current={pagination.page}
+                pageSize={pagination.pageSize}
+                total={pagination.total}
+                showSizeChanger
+                showQuickJumper
+                showTotal={(total) => `共 ${total} 条`}
+                onChange={(page, pageSize) => loadList(page, pageSize)}
+              />
+            </div>
+          ) : null}
+        </div>
+      </div>
+
+      <Modal
+        open={modalOpen}
+        title={isDetailMode ? '查看APP版本' : '新增APP版本'}
+        className="app-version-settings-modal"
+        width={760}
+        destroyOnClose
+        centered
+        onCancel={handleCloseModal}
+        footer={[
+          <Button key="cancel" onClick={handleCloseModal} disabled={submitting}>
+            {isDetailMode ? '关闭' : '取消'}
+          </Button>,
+          isDetailMode ? null : (
+            <Button key="submit" type="primary" loading={submitting} onClick={handleSubmit}>
+              确定
+            </Button>
+          ),
+        ]}
+      >
+        <Spin spinning={modalLoading}>
+          <Form
+            form={form}
+            layout="horizontal"
+            className="app-version-settings-form"
+            disabled={isDetailMode}
+            labelCol={{ flex: '78px' }}
+            wrapperCol={{ flex: '1' }}
+          >
+            <Form.Item
+              label="版本号"
+              name="versionNo"
+              rules={[
+                { required: true, message: '请输入版本号' },
+                { max: 32, message: '版本号长度不能超过32个字符' },
+              ]}
+              className="app-version-settings-form__version-item"
+            >
+              <Input placeholder="请输入" maxLength={32} />
+            </Form.Item>
+
+            <div className="app-version-settings-form__aligned-content">
+              <div className="app-version-settings-form__upload-row">
+                <div className="app-version-settings-form__upload-col">
+                  <div className="app-version-settings-form__upload-label">安卓文件:</div>
+                  <Form.Item name="androidFileUrl" rules={[{ required: true, message: '请上传安卓文件' }]} hidden>
+                    <Input />
+                  </Form.Item>
+                  <Form.Item name="androidFileName" rules={[{ required: true, message: '请填写安卓文件名称' }]} hidden>
+                    <Input />
+                  </Form.Item>
+                  <label className="app-version-settings-form__upload-trigger">
+                    <input
+                      type="file"
+                      accept=".apk"
+                      hidden
+                      disabled={isDetailMode}
+                      onChange={async (event) => {
+                        const file = event.target.files?.[0];
+                        event.target.value = '';
+                        if (!file) {
+                          return;
+                        }
+                        await handlePackageUpload(file, 'ANDROID');
+                      }}
+                    />
+                    <Button icon={<UploadOutlined />} loading={uploadingAndroid} disabled={isDetailMode}>
+                      上传
+                    </Button>
+                  </label>
+                  <div className="app-version-settings-form__file-name">{androidFileName || '未上传文件'}</div>
+                </div>
+
+                <div className="app-version-settings-form__upload-col">
+                  <div className="app-version-settings-form__upload-label">ios文件:</div>
+                  <Form.Item name="iosFileUrl" rules={[{ required: true, message: '请上传iOS文件' }]} hidden>
+                    <Input />
+                  </Form.Item>
+                  <Form.Item name="iosFileName" rules={[{ required: true, message: '请填写iOS文件名称' }]} hidden>
+                    <Input />
+                  </Form.Item>
+                  <label className="app-version-settings-form__upload-trigger">
+                    <input
+                      type="file"
+                      accept=".ipa"
+                      hidden
+                      disabled={isDetailMode}
+                      onChange={async (event) => {
+                        const file = event.target.files?.[0];
+                        event.target.value = '';
+                        if (!file) {
+                          return;
+                        }
+                        await handlePackageUpload(file, 'IOS');
+                      }}
+                    />
+                    <Button icon={<UploadOutlined />} loading={uploadingIos} disabled={isDetailMode}>
+                      上传
+                    </Button>
+                  </label>
+                  <div className="app-version-settings-form__file-name">{iosFileName || '未上传文件'}</div>
+                </div>
+              </div>
+            </div>
+
+            <Form.Item label="强制更新" name="forceUpdate" valuePropName="checked">
+              <Switch />
+            </Form.Item>
+
+            <Form.Item
+              label="更新说明"
+              name="updateNotice"
+              rules={[
+                { required: true, message: '请输入更新说明' },
+                { max: 2000, message: '更新说明长度不能超过2000个字符' },
+              ]}
+              className="app-version-settings-form__textarea-item"
+            >
+              <Input.TextArea rows={6} maxLength={2000} placeholder="请输入" />
+            </Form.Item>
+          </Form>
+        </Spin>
+      </Modal>
+    </div>
+  );
+};
+
+export default AppVersionSettingsPage;
diff --git a/tcm-training-admin/src/pages/system/app-version-settings/service.ts b/tcm-training-admin/src/pages/system/app-version-settings/service.ts
new file mode 100644
index 0000000..15e636d
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/app-version-settings/service.ts
@@ -0,0 +1,31 @@
+import { request } from '@umijs/max';
+
+export async function getAppVersionPage(params?: Record<string, any>) {
+  return request('/app-version-settings', {
+    method: 'GET',
+    params,
+  });
+}
+
+export async function getAppVersionDetail(id: number | string) {
+  return request(`/app-version-settings/${id}`, {
+    method: 'GET',
+  });
+}
+
+export async function createAppVersion(data: Record<string, any>) {
+  return request('/app-version-settings', {
+    method: 'POST',
+    data,
+  });
+}
+
+export async function uploadAppPackage(file: File, platform: 'ANDROID' | 'IOS') {
+  const formData = new FormData();
+  formData.append('file', file);
+  formData.append('platform', platform);
+  return request('/upload/app-package', {
+    method: 'POST',
+    data: formData,
+  });
+}
diff --git a/tcm-training-admin/src/pages/system/app-version-settings/style.less b/tcm-training-admin/src/pages/system/app-version-settings/style.less
new file mode 100644
index 0000000..be54634
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/app-version-settings/style.less
@@ -0,0 +1,153 @@
+.app-version-settings-page {
+  min-height: calc(100vh - 48px);
+  padding: 18px 20px 24px;
+  background: #f2f5f9;
+}
+
+.app-version-settings-panel {
+  min-height: 700px;
+  padding: 18px 18px 24px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.app-version-settings-panel__title {
+  margin-bottom: 18px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 20px;
+}
+
+.app-version-settings-toolbar {
+  display: flex;
+  justify-content: flex-start;
+  margin-bottom: 10px;
+}
+
+.app-version-settings-table-card {
+  min-height: 560px;
+  overflow: hidden;
+  border: 1px solid #e6ebf2;
+  border-radius: 4px;
+}
+
+.app-version-settings-table-card .ant-table-thead > tr > th {
+  color: #303133;
+  font-weight: 500;
+  background: #f5f7fa;
+}
+
+.app-version-settings-table-card .ant-table-tbody > tr > td {
+  color: #606266;
+}
+
+.app-version-settings-table__link {
+  padding: 0;
+  color: #409eff;
+  background: transparent;
+  border: 0;
+  cursor: pointer;
+}
+
+.app-version-settings-pagination {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 18px;
+  background: #fff;
+}
+
+.app-version-settings-modal .ant-modal-content {
+  padding: 14px 18px 18px;
+  border-radius: 8px;
+}
+
+.app-version-settings-modal .ant-modal-header {
+  margin-bottom: 12px;
+}
+
+.app-version-settings-modal .ant-modal-title {
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.app-version-settings-modal .ant-modal-close {
+  top: 18px;
+}
+
+.app-version-settings-form .ant-form-item {
+  margin-bottom: 16px;
+}
+
+.app-version-settings-form {
+  --app-version-label-width: 78px;
+}
+
+.app-version-settings-form__version-item {
+  max-width: 340px;
+}
+
+.app-version-settings-form__aligned-content {
+  padding-left: var(--app-version-label-width);
+}
+
+.app-version-settings-form__upload-row {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 18px 56px;
+  margin-bottom: 12px;
+}
+
+.app-version-settings-form__upload-col {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  align-items: flex-start;
+  min-width: 0;
+}
+
+.app-version-settings-form__upload-label {
+  color: #303133;
+  font-size: 14px;
+}
+
+.app-version-settings-form__upload-trigger .ant-btn {
+  min-width: 110px;
+}
+
+.app-version-settings-form__file-name {
+  max-width: 240px;
+  color: #606266;
+  font-size: 12px;
+  line-height: 18px;
+  word-break: break-all;
+}
+
+.app-version-settings-form__textarea-item {
+  margin-top: 4px;
+}
+
+@media (max-width: 768px) {
+  .app-version-settings-page {
+    padding: 12px;
+  }
+
+  .app-version-settings-panel {
+    min-height: auto;
+    padding: 16px;
+  }
+
+  .app-version-settings-form__upload-row {
+    grid-template-columns: 1fr;
+    gap: 16px;
+  }
+
+  .app-version-settings-form__aligned-content {
+    padding-left: 0;
+  }
+
+  .app-version-settings-form__version-item {
+    max-width: none;
+  }
+}
diff --git a/tcm-training-admin/src/pages/system/banner-settings/index.tsx b/tcm-training-admin/src/pages/system/banner-settings/index.tsx
new file mode 100644
index 0000000..8a25d71
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/banner-settings/index.tsx
@@ -0,0 +1,711 @@
+import {
+  batchDeleteSystemBanner,
+  createSystemBanner,
+  deleteSystemBanner,
+  getSystemBannerDetail,
+  getSystemBannerHealthCourseOptions,
+  getSystemBannerLiveCourseOptions,
+  getSystemBannerPage,
+  updateSystemBanner,
+  updateSystemBannerPublishStatus,
+  uploadSystemBannerImage,
+} from './service';
+import { PlusOutlined } from '@ant-design/icons';
+import { history, useAccess } from '@umijs/max';
+import {
+  Button,
+  DatePicker,
+  Form,
+  Image,
+  Input,
+  Modal,
+  Pagination,
+  Select,
+  Space,
+  Spin,
+  Switch,
+  Table,
+  Tag,
+  Upload,
+  message,
+} from 'antd';
+import dayjs from 'dayjs';
+import type { ColumnsType } from 'antd/es/table';
+import React, { useEffect, useMemo, useState } from 'react';
+import './style.less';
+
+type SearchValues = {
+  status?: number;
+  title?: string;
+};
+
+type BannerItem = {
+  bannerImg?: string;
+  createdAt: string;
+  endAt: string;
+  id: number;
+  jumpTargetDisplay?: string;
+  jumpType: string;
+  publishEnabled: boolean;
+  startAt: string;
+  status: number;
+  title: string;
+};
+
+type BannerDetail = {
+  bannerImg?: string;
+  createdAt?: string;
+  endAt?: string;
+  id: number;
+  jumpType?: string;
+  publishEnabled?: boolean;
+  startAt?: string;
+  status?: number;
+  targetId?: number;
+  targetName?: string;
+  targetUrl?: string;
+  title?: string;
+  updatedAt?: string;
+};
+
+type CourseOption = {
+  id: number;
+  title: string;
+};
+
+type BannerFormValues = {
+  bannerImg?: string;
+  endAt?: any;
+  jumpType?: string;
+  publishEnabled?: boolean;
+  startAt?: any;
+  targetId?: number;
+  targetName?: string;
+  targetUrl?: string;
+  title?: string;
+};
+
+type ModalMode = 'create' | 'edit' | 'detail';
+
+const JUMP_TYPE_HEALTH_COURSE = 'HEALTH_COURSE';
+const JUMP_TYPE_LIVE_COURSE = 'LIVE_COURSE';
+const JUMP_TYPE_MEMBER = 'MEMBER';
+const JUMP_TYPE_THIRD_PARTY_URL = 'THIRD_PARTY_URL';
+
+const jumpTypeOptions = [
+  { label: '养生课', value: JUMP_TYPE_HEALTH_COURSE },
+  { label: '直播课', value: JUMP_TYPE_LIVE_COURSE },
+  { label: '会员', value: JUMP_TYPE_MEMBER },
+  { label: '三方地址', value: JUMP_TYPE_THIRD_PARTY_URL },
+];
+
+const statusOptions = [
+  { label: '未开始', value: 1 },
+  { label: '进行中', value: 2 },
+  { label: '已结束', value: 3 },
+];
+
+const defaultValues: BannerFormValues = {
+  bannerImg: '',
+  title: '',
+  jumpType: JUMP_TYPE_MEMBER,
+  targetId: undefined,
+  targetName: '',
+  targetUrl: '',
+  publishEnabled: true,
+};
+
+const trimSearchValues = (values: SearchValues) => ({
+  title: values.title?.trim() || undefined,
+  status: values.status,
+});
+
+const BannerSettingsPage: React.FC = () => {
+  const access = useAccess();
+  const [searchForm] = Form.useForm<SearchValues>();
+  const [form] = Form.useForm<BannerFormValues>();
+  const [list, setList] = useState<BannerItem[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+  const [modalOpen, setModalOpen] = useState(false);
+  const [modalMode, setModalMode] = useState<ModalMode>('create');
+  const [editingId, setEditingId] = useState<number>();
+  const [modalLoading, setModalLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [uploading, setUploading] = useState(false);
+  const [courseOptions, setCourseOptions] = useState<CourseOption[]>([]);
+  const [optionsLoading, setOptionsLoading] = useState(false);
+
+  const canView = access['system:banner:view'];
+  const canAdd = access['system:banner:add'];
+  const canEdit = access['system:banner:edit'];
+  const canDetail = access['system:banner:detail'];
+  const canDelete = access['system:banner:delete'];
+  const canBatchDelete = access['system:banner:batch-delete'];
+  const canPublishStatus = access['system:banner:publish-status'];
+  const isDetailMode = modalMode === 'detail';
+  const jumpType = Form.useWatch('jumpType', form);
+  const bannerImg = Form.useWatch('bannerImg', form);
+
+  const showCourseField =
+    jumpType === JUMP_TYPE_HEALTH_COURSE || jumpType === JUMP_TYPE_LIVE_COURSE;
+  const showUrlField = jumpType === JUMP_TYPE_THIRD_PARTY_URL;
+
+  const coursePlaceholder = useMemo(() => {
+    if (jumpType === JUMP_TYPE_HEALTH_COURSE) {
+      return '请输入养生课程';
+    }
+    if (jumpType === JUMP_TYPE_LIVE_COURSE) {
+      return '请输入直播课程';
+    }
+    return '请输入课程';
+  }, [jumpType]);
+
+  const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => {
+    setLoading(true);
+    try {
+      const res = await getSystemBannerPage({
+        page,
+        pageSize,
+        ...trimSearchValues(searchForm.getFieldsValue()),
+      });
+      setList(res.data?.list || []);
+      setPagination({
+        page: Number(res.data?.page || page),
+        pageSize: Number(res.data?.pageSize || pageSize),
+        total: Number(res.data?.total || 0),
+      });
+      setSelectedRowKeys([]);
+    } catch (error) {
+      message.error((error as Error)?.message || 'Banner列表加载失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!canView) {
+      history.replace('/Welcome');
+      return;
+    }
+    loadList(1, 10);
+  }, [canView]);
+
+  const loadCourseOptions = async (keyword?: string, type?: string) => {
+    const currentType = type || form.getFieldValue('jumpType');
+    if (currentType !== JUMP_TYPE_HEALTH_COURSE && currentType !== JUMP_TYPE_LIVE_COURSE) {
+      setCourseOptions([]);
+      return;
+    }
+    setOptionsLoading(true);
+    try {
+      const res =
+        currentType === JUMP_TYPE_HEALTH_COURSE
+          ? await getSystemBannerHealthCourseOptions(keyword)
+          : await getSystemBannerLiveCourseOptions(keyword);
+      setCourseOptions(res.data || []);
+    } catch (error) {
+      message.error((error as Error)?.message || '课程选项加载失败');
+    } finally {
+      setOptionsLoading(false);
+    }
+  };
+
+  const openCreateModal = () => {
+    setModalMode('create');
+    setEditingId(undefined);
+    setModalLoading(false);
+    setCourseOptions([]);
+    form.resetFields();
+    form.setFieldsValue(defaultValues);
+    setModalOpen(true);
+  };
+
+  const openDetailModal = async (record: BannerItem, mode: ModalMode) => {
+    setModalMode(mode);
+    setEditingId(record.id);
+    setModalOpen(true);
+    setModalLoading(true);
+    setCourseOptions([]);
+    form.resetFields();
+    try {
+      const res = await getSystemBannerDetail(record.id);
+      const detail: BannerDetail = res.data;
+      const nextValues: BannerFormValues = {
+        bannerImg: detail.bannerImg || '',
+        title: detail.title || '',
+        startAt: detail.startAt ? dayjs(detail.startAt) : undefined,
+        endAt: detail.endAt ? dayjs(detail.endAt) : undefined,
+        jumpType: detail.jumpType || JUMP_TYPE_MEMBER,
+        targetId: detail.targetId,
+        targetName: detail.targetName || '',
+        targetUrl: detail.targetUrl || '',
+        publishEnabled: detail.publishEnabled ?? true,
+      };
+      form.setFieldsValue(nextValues);
+      if (
+        (detail.jumpType === JUMP_TYPE_HEALTH_COURSE || detail.jumpType === JUMP_TYPE_LIVE_COURSE) &&
+        detail.targetId
+      ) {
+        setCourseOptions([{ id: detail.targetId, title: detail.targetName || '' }]);
+      }
+    } catch (error) {
+      message.error((error as Error)?.message || 'Banner详情加载失败');
+      setModalOpen(false);
+    } finally {
+      setModalLoading(false);
+    }
+  };
+
+  const handleCloseModal = () => {
+    if (submitting || uploading) {
+      return;
+    }
+    setModalOpen(false);
+    setModalLoading(false);
+    setCourseOptions([]);
+    form.resetFields();
+  };
+
+  const handleDelete = (record: BannerItem) => {
+    Modal.confirm({
+      title: '确定要删除该Banner吗?',
+      okText: '删除',
+      cancelText: '取消',
+      onOk: async () => {
+        await deleteSystemBanner(record.id);
+        message.success('删除成功');
+        const nextPage = list.length === 1 && pagination.page > 1 ? pagination.page - 1 : pagination.page;
+        await loadList(nextPage, pagination.pageSize);
+      },
+    });
+  };
+
+  const handleBatchDelete = () => {
+    if (!selectedRowKeys.length) {
+      return;
+    }
+    Modal.confirm({
+      title: '确定要批量删除选中的Banner吗?',
+      okText: '删除',
+      cancelText: '取消',
+      onOk: async () => {
+        await batchDeleteSystemBanner(selectedRowKeys as number[]);
+        message.success('批量删除成功');
+        const shouldBackPage = list.length === selectedRowKeys.length && pagination.page > 1;
+        await loadList(shouldBackPage ? pagination.page - 1 : pagination.page, pagination.pageSize);
+      },
+    });
+  };
+
+  const handleSubmit = async () => {
+    if (isDetailMode) {
+      handleCloseModal();
+      return;
+    }
+    const values = await form.validateFields();
+    const data = {
+      bannerImg: values.bannerImg?.trim(),
+      title: values.title?.trim(),
+      startAt: dayjs(values.startAt).format('YYYY-MM-DD HH:mm:ss'),
+      endAt: dayjs(values.endAt).format('YYYY-MM-DD HH:mm:ss'),
+      jumpType: values.jumpType,
+      targetId: showCourseField ? values.targetId : undefined,
+      targetName: showCourseField ? values.targetName?.trim() : undefined,
+      targetUrl: showUrlField ? values.targetUrl?.trim() : undefined,
+      publishEnabled: !!values.publishEnabled,
+    };
+    setSubmitting(true);
+    try {
+      if (modalMode === 'create') {
+        await createSystemBanner(data);
+        message.success('新增成功');
+      } else if (editingId) {
+        await updateSystemBanner(editingId, data);
+        message.success('保存成功');
+      }
+      setModalOpen(false);
+      form.resetFields();
+      await loadList(modalMode === 'create' ? 1 : pagination.page, pagination.pageSize);
+    } catch (error) {
+      message.error((error as Error)?.message || '保存失败,请稍后重试');
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const columns: ColumnsType<BannerItem> = [
+    {
+      title: 'banner图',
+      dataIndex: 'bannerImg',
+      width: 140,
+      render: (value) => (
+        <div className="banner-settings-table__image">
+          {value ? <Image src={value} preview={false} width={110} height={42} /> : null}
+        </div>
+      ),
+    },
+    {
+      title: '标题',
+      dataIndex: 'title',
+      width: 160,
+      ellipsis: true,
+    },
+    {
+      title: '显示时间',
+      width: 220,
+      render: (_, record) => (
+        <div className="banner-settings-table__time">
+          <div>{record.startAt || '-'}</div>
+          <div>{record.endAt || '-'}</div>
+        </div>
+      ),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      width: 100,
+      render: (value) => {
+        if (value === 1) {
+          return <Tag color="cyan">未开始</Tag>;
+        }
+        if (value === 2) {
+          return <Tag color="green">进行中</Tag>;
+        }
+        return <Tag>已结束</Tag>;
+      },
+    },
+    {
+      title: '上架状态',
+      dataIndex: 'publishEnabled',
+      width: 120,
+      render: (value, record) => (
+        <Switch
+          checked={!!value}
+          disabled={!canPublishStatus}
+          onChange={async (checked) => {
+            try {
+              await updateSystemBannerPublishStatus(record.id, checked);
+              message.success('上架状态更新成功');
+              await loadList(pagination.page, pagination.pageSize);
+            } catch (error) {
+              message.error((error as Error)?.message || '上架状态更新失败');
+            }
+          }}
+        />
+      ),
+    },
+    {
+      title: '跳转地址',
+      dataIndex: 'jumpTargetDisplay',
+      width: 180,
+      ellipsis: true,
+      render: (value) => value || '-',
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createdAt',
+      width: 180,
+    },
+    {
+      title: '操作',
+      width: 200,
+      render: (_, record) => (
+        <Space size={8}>
+          {canDetail ? (
+            <Button size="small" onClick={() => openDetailModal(record, 'detail')}>
+              查看
+            </Button>
+          ) : null}
+          {canEdit ? (
+            <Button size="small" onClick={() => openDetailModal(record, 'edit')}>
+              编辑
+            </Button>
+          ) : null}
+          {canDelete ? (
+            <Button size="small" onClick={() => handleDelete(record)}>
+              删除
+            </Button>
+          ) : null}
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="banner-settings-page">
+      <div className="banner-settings-card">
+        <div className="banner-settings-card__title">banner管理</div>
+        <div className="banner-settings-search-card">
+          <Form form={searchForm} layout="horizontal" className="banner-settings-search-form">
+            <Form.Item label="标题" name="title">
+              <Input placeholder="请输入" allowClear />
+            </Form.Item>
+            <Form.Item label="状态" name="status">
+              <Select placeholder="请选择" allowClear options={statusOptions} />
+            </Form.Item>
+            <div className="banner-settings-search-form__actions">
+              <Button type="primary" onClick={() => loadList(1, pagination.pageSize)}>
+                查询
+              </Button>
+              <Button
+                onClick={async () => {
+                  searchForm.resetFields();
+                  await loadList(1, pagination.pageSize);
+                }}
+              >
+                重置
+              </Button>
+            </div>
+          </Form>
+        </div>
+
+        <div className="banner-settings-toolbar">
+          {canBatchDelete ? (
+            <Button danger disabled={!selectedRowKeys.length} onClick={handleBatchDelete}>
+              批量删除
+            </Button>
+          ) : (
+            <span />
+          )}
+          {canAdd ? (
+            <Button type="primary" onClick={openCreateModal}>
+              添加
+            </Button>
+          ) : null}
+        </div>
+
+        <div className="banner-settings-table-card">
+          <Table<BannerItem>
+            rowKey="id"
+            columns={columns}
+            dataSource={list}
+            loading={loading}
+            pagination={false}
+            scroll={{ x: 1280 }}
+            rowSelection={
+              canBatchDelete
+                ? {
+                    selectedRowKeys,
+                    onChange: (keys) => setSelectedRowKeys(keys),
+                  }
+                : undefined
+            }
+          />
+          <div className="banner-settings-pagination">
+            <Pagination
+              current={pagination.page}
+              pageSize={pagination.pageSize}
+              total={pagination.total}
+              showSizeChanger
+              showQuickJumper
+              showTotal={(total) => `共 ${total} 条`}
+              onChange={(page, pageSize) => loadList(page, pageSize)}
+            />
+          </div>
+        </div>
+      </div>
+
+      <Modal
+        open={modalOpen}
+        className="banner-settings-modal"
+        destroyOnClose
+        centered
+        width={680}
+        title={modalMode === 'create' ? '添加banner' : modalMode === 'edit' ? '编辑banner' : '查看banner'}
+        onCancel={handleCloseModal}
+        footer={[
+          <Button key="cancel" onClick={handleCloseModal} disabled={submitting}>
+            {isDetailMode ? '关闭' : '取消'}
+          </Button>,
+          isDetailMode ? null : (
+            <Button key="submit" type="primary" loading={submitting} onClick={handleSubmit}>
+              确定
+            </Button>
+          ),
+        ]}
+      >
+        <Spin spinning={modalLoading}>
+          <Form
+            form={form}
+            className="banner-settings-form"
+            layout="horizontal"
+            disabled={isDetailMode}
+            labelCol={{ flex: '118px' }}
+            wrapperCol={{ flex: '1' }}
+            initialValues={defaultValues}
+            onValuesChange={(changedValues) => {
+              if (!Object.prototype.hasOwnProperty.call(changedValues, 'jumpType')) {
+                return;
+              }
+              const nextType = changedValues.jumpType;
+              form.setFieldsValue({
+                targetId: undefined,
+                targetName: '',
+                targetUrl: nextType === JUMP_TYPE_THIRD_PARTY_URL ? form.getFieldValue('targetUrl') : '',
+              });
+              setCourseOptions([]);
+              if (nextType === JUMP_TYPE_HEALTH_COURSE || nextType === JUMP_TYPE_LIVE_COURSE) {
+                loadCourseOptions('', nextType);
+              }
+            }}
+          >
+            <Form.Item label="banner图" required className="banner-settings-form__upload-item">
+              <Form.Item name="bannerImg" rules={[{ required: true, message: '请上传banner图' }]} hidden>
+                <Input />
+              </Form.Item>
+              <div className="banner-settings-form__upload-content">
+                <Upload
+                  accept="image/png,image/jpeg"
+                  showUploadList={false}
+                  disabled={isDetailMode}
+                  customRequest={async (options) => {
+                    setUploading(true);
+                    try {
+                      const file = options.file as File;
+                      const res = await uploadSystemBannerImage(file);
+                      const url = res.data?.url;
+                      if (!url) {
+                        throw new Error('上传失败');
+                      }
+                      form.setFieldsValue({ bannerImg: url });
+                      options.onSuccess?.(res);
+                    } catch (error) {
+                      message.error((error as Error)?.message || '图片上传失败');
+                      options.onError?.(error as Error);
+                    } finally {
+                      setUploading(false);
+                    }
+                  }}
+                >
+                  <div className="banner-settings-form__upload-box">
+                    {bannerImg ? (
+                      <Image src={bannerImg} preview={false} width={96} height={96} />
+                    ) : (
+                      <div className="banner-settings-form__upload-placeholder">
+                        <PlusOutlined />
+                        <span>上传照片</span>
+                      </div>
+                    )}
+                  </div>
+                </Upload>
+                <div className="banner-settings-form__upload-tip">
+                  <div>支持jpg、png格式 banner建议尺寸:X px Xpx</div>
+                  <div>弹出banner建议尺寸:X px Xpx</div>
+                </div>
+              </div>
+            </Form.Item>
+
+            <Form.Item
+              label="banner标题"
+              name="title"
+              rules={[
+                { required: true, message: '请输入banner标题' },
+                { max: 64, message: 'banner标题长度不能超过64个字符' },
+              ]}
+            >
+              <Input placeholder="请输入" maxLength={64} />
+            </Form.Item>
+
+            <Form.Item
+              label="banner开始时间"
+              name="startAt"
+              rules={[{ required: true, message: '请选择banner开始时间' }]}
+            >
+              <DatePicker
+                showTime
+                format="YYYY-MM-DD HH:mm:ss"
+                placeholder="年 / 月 / 日"
+                className="banner-settings-form__date"
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="banner结束时间"
+              name="endAt"
+              rules={[
+                { required: true, message: '请选择banner结束时间' },
+                ({ getFieldValue }) => ({
+                  validator(_, value) {
+                    const startAt = getFieldValue('startAt');
+                    if (!value || !startAt || dayjs(value).isAfter(dayjs(startAt))) {
+                      return Promise.resolve();
+                    }
+                    return Promise.reject(new Error('banner结束时间必须晚于开始时间'));
+                  },
+                }),
+              ]}
+            >
+              <DatePicker
+                showTime
+                format="YYYY-MM-DD HH:mm:ss"
+                placeholder="年 / 月 / 日"
+                className="banner-settings-form__date"
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="跳转类型"
+              name="jumpType"
+              rules={[{ required: true, message: '请选择跳转类型' }]}
+            >
+              <Select placeholder="请选择" options={jumpTypeOptions} />
+            </Form.Item>
+
+            {showCourseField ? (
+              <Form.Item
+                label="课程"
+                name="targetId"
+                rules={[
+                  {
+                    required: true,
+                    message: jumpType === JUMP_TYPE_HEALTH_COURSE ? '请选择养生课程' : '请选择直播课程',
+                  },
+                ]}
+              >
+                <Select
+                  showSearch
+                  placeholder={coursePlaceholder}
+                  filterOption={false}
+                  options={courseOptions.map((item) => ({ label: item.title, value: item.id }))}
+                  loading={optionsLoading}
+                  onFocus={() => loadCourseOptions('', jumpType)}
+                  onSearch={(keyword) => loadCourseOptions(keyword, jumpType)}
+                  onChange={(value, option) => {
+                    const selectedOption = Array.isArray(option) ? option[0] : option;
+                    form.setFieldValue('targetName', selectedOption?.label || '');
+                  }}
+                />
+              </Form.Item>
+            ) : null}
+
+            {showUrlField ? (
+              <Form.Item
+                label="地址信息"
+                name="targetUrl"
+                rules={[
+                  { required: true, message: '请输入地址信息' },
+                  { max: 512, message: '地址信息长度不能超过512个字符' },
+                ]}
+              >
+                <Input placeholder="请输入" maxLength={512} />
+              </Form.Item>
+            ) : null}
+
+            <Form.Item
+              label="banner上架状态"
+              name="publishEnabled"
+              valuePropName="checked"
+              className="banner-settings-form__switch-item"
+            >
+              <Switch />
+            </Form.Item>
+          </Form>
+        </Spin>
+      </Modal>
+    </div>
+  );
+};
+
+export default BannerSettingsPage;
diff --git a/tcm-training-admin/src/pages/system/banner-settings/service.ts b/tcm-training-admin/src/pages/system/banner-settings/service.ts
new file mode 100644
index 0000000..b0d888e
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/banner-settings/service.ts
@@ -0,0 +1,74 @@
+import { request } from '@umijs/max';
+
+export async function getSystemBannerPage(params?: Record<string, any>) {
+  return request('/system-banners', {
+    method: 'GET',
+    params,
+  });
+}
+
+export async function getSystemBannerDetail(id: number | string) {
+  return request(`/system-banners/${id}`, {
+    method: 'GET',
+  });
+}
+
+export async function createSystemBanner(data: Record<string, any>) {
+  return request('/system-banners', {
+    method: 'POST',
+    data,
+  });
+}
+
+export async function updateSystemBanner(id: number | string, data: Record<string, any>) {
+  return request(`/system-banners/${id}`, {
+    method: 'PUT',
+    data,
+  });
+}
+
+export async function deleteSystemBanner(id: number | string) {
+  return request(`/system-banners/${id}`, {
+    method: 'DELETE',
+  });
+}
+
+export async function batchDeleteSystemBanner(ids: Array<number | string>) {
+  return request('/system-banners/batch', {
+    method: 'DELETE',
+    data: { ids },
+  });
+}
+
+export async function updateSystemBannerPublishStatus(
+  id: number | string,
+  publishEnabled: boolean,
+) {
+  return request(`/system-banners/${id}/publish-status`, {
+    method: 'PATCH',
+    data: { publishEnabled },
+  });
+}
+
+export async function getSystemBannerHealthCourseOptions(keyword?: string) {
+  return request('/system-banners/health-course-options', {
+    method: 'GET',
+    params: { keyword },
+  });
+}
+
+export async function getSystemBannerLiveCourseOptions(keyword?: string) {
+  return request('/system-banners/live-course-options', {
+    method: 'GET',
+    params: { keyword },
+  });
+}
+
+export async function uploadSystemBannerImage(file: File) {
+  const formData = new FormData();
+  formData.append('file', file);
+  return request('/upload/image', {
+    method: 'POST',
+    data: formData,
+  });
+}
diff --git a/tcm-training-admin/src/pages/system/banner-settings/style.less b/tcm-training-admin/src/pages/system/banner-settings/style.less
new file mode 100644
index 0000000..2ca51f4
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/banner-settings/style.less
@@ -0,0 +1,254 @@
+.banner-settings-page {
+  min-height: calc(100vh - 48px);
+  padding: 18px 20px 24px;
+  background: #f2f5f9;
+}
+
+.banner-settings-card {
+  min-height: 700px;
+  padding: 8px 14px 18px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.banner-settings-card__title {
+  margin: 0 0 22px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.banner-settings-search-card {
+  margin-bottom: 20px;
+}
+
+.banner-settings-search-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0 18px;
+  align-items: center;
+}
+
+.banner-settings-search-form .ant-form-item {
+  margin-bottom: 0;
+}
+
+.banner-settings-search-form .ant-input,
+.banner-settings-search-form .ant-select {
+  width: 170px;
+}
+
+.banner-settings-search-form__actions {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.banner-settings-toolbar {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 14px;
+}
+
+.banner-settings-table-card {
+  overflow: hidden;
+  border: 1px solid #e6ebf2;
+  border-radius: 4px;
+}
+
+.banner-settings-table-card .ant-table-thead > tr > th {
+  color: #303133;
+  font-weight: 500;
+  background: #f5f7fa;
+}
+
+.banner-settings-table-card .ant-table-tbody > tr > td {
+  color: #606266;
+  vertical-align: middle;
+}
+
+.banner-settings-table__image {
+  width: 110px;
+  height: 42px;
+  overflow: hidden;
+  border-radius: 2px;
+}
+
+.banner-settings-table__image .ant-image,
+.banner-settings-table__image .ant-image-img {
+  width: 110px;
+  height: 42px;
+}
+
+.banner-settings-table__time {
+  line-height: 20px;
+  word-break: break-all;
+}
+
+.banner-settings-pagination {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 18px;
+  background: #fff;
+}
+
+.banner-settings-modal .ant-modal-content {
+  padding: 18px 24px 20px;
+  border-radius: 8px;
+}
+
+.banner-settings-modal .ant-modal-title {
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.banner-settings-modal .ant-modal-body {
+  padding-top: 12px;
+}
+
+.banner-settings-modal .ant-modal-footer {
+  padding-top: 12px;
+}
+
+.banner-settings-form {
+  padding: 4px 6px 0 0;
+}
+
+.banner-settings-form .ant-form-item {
+  margin-bottom: 18px;
+}
+
+.banner-settings-form .ant-form-item-label > label {
+  color: #303133;
+}
+
+.banner-settings-form .ant-form-item-control-input-content > .ant-input,
+.banner-settings-form .ant-form-item-control-input-content > .ant-picker,
+.banner-settings-form .ant-form-item-control-input-content > .ant-select {
+  width: 100%;
+}
+
+.banner-settings-form .ant-select-selector,
+.banner-settings-form .ant-input,
+.banner-settings-form .ant-picker {
+  min-height: 40px;
+}
+
+.banner-settings-form .ant-input,
+.banner-settings-form .ant-picker-input > input {
+  font-size: 14px;
+}
+
+.banner-settings-form__upload-item {
+  align-items: flex-start;
+}
+
+.banner-settings-form__upload-item .ant-form-item-control {
+  min-height: 96px;
+}
+
+.banner-settings-form__upload-content {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px 16px;
+  align-items: flex-start;
+}
+
+.banner-settings-form__upload-box {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex: 0 0 96px;
+  width: 96px;
+  height: 96px;
+  overflow: hidden;
+  background: #fff;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.banner-settings-form__upload-box .ant-image,
+.banner-settings-form__upload-box .ant-image-img {
+  width: 96px;
+  height: 96px;
+}
+
+.banner-settings-form__upload-placeholder {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  align-items: center;
+  color: #909399;
+  font-size: 12px;
+}
+
+.banner-settings-form__upload-tip {
+  flex: 1;
+  min-width: 240px;
+  padding-top: 6px;
+  color: #c0c4cc;
+  font-size: 12px;
+  line-height: 18px;
+  word-break: break-word;
+}
+
+.banner-settings-form__date {
+  width: 100%;
+}
+
+.banner-settings-form__switch-item {
+  margin-top: 8px;
+  margin-bottom: 0;
+}
+
+@media (max-width: 900px) {
+  .banner-settings-page {
+    padding: 12px;
+  }
+
+  .banner-settings-card {
+    min-height: auto;
+    padding: 16px;
+  }
+
+  .banner-settings-search-form {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .banner-settings-search-form .ant-input,
+  .banner-settings-search-form .ant-select {
+    width: 100%;
+  }
+
+  .banner-settings-search-form__actions,
+  .banner-settings-toolbar {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .banner-settings-modal {
+    width: calc(100vw - 24px) !important;
+    max-width: calc(100vw - 24px);
+  }
+
+  .banner-settings-modal .ant-modal-content {
+    padding: 16px;
+  }
+
+  .banner-settings-form {
+    padding-right: 0;
+  }
+
+  .banner-settings-form .ant-form-item {
+    margin-bottom: 16px;
+  }
+
+  .banner-settings-form__upload-tip {
+    min-width: 100%;
+    padding-top: 0;
+  }
+}
diff --git a/tcm-training-admin/src/pages/system/basic-settings/index.tsx b/tcm-training-admin/src/pages/system/basic-settings/index.tsx
new file mode 100644
index 0000000..828425a
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/basic-settings/index.tsx
@@ -0,0 +1,201 @@
+import { history, useAccess } from '@umijs/max';
+import { Button, Form, Input, Spin, message } from 'antd';
+import React, { useEffect, useState } from 'react';
+import { getCurrentSystemSettings, saveCurrentSystemSettings } from './service';
+import './style.less';
+
+type SystemSettingsValues = {
+  backupCycleDays?: string;
+  memberExpireRemindDays?: string;
+  serviceOnlineTime?: string;
+  servicePhone?: string;
+};
+
+const validatePositiveInteger = (label: string) => (_: unknown, value: string | undefined) => {
+  if (value === undefined || value === null || String(value).trim() === '') {
+    return Promise.resolve();
+  }
+  if (/^[1-9]\d*$/.test(String(value).trim())) {
+    return Promise.resolve();
+  }
+  return Promise.reject(new Error(`${label}必须为大于0的整数`));
+};
+
+const SystemSettingsPage: React.FC = () => {
+  const access = useAccess();
+  const [form] = Form.useForm<SystemSettingsValues>();
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+
+  const canView = access['system:basic-settings:view'];
+  const canEdit = access['system:basic-settings:edit'];
+
+  const loadCurrent = async () => {
+    setLoading(true);
+    try {
+      const res = await getCurrentSystemSettings();
+      const data = res.data;
+      form.resetFields();
+      form.setFieldsValue({
+        servicePhone: data?.servicePhone || '',
+        serviceOnlineTime: data?.serviceOnlineTime || '',
+        backupCycleDays:
+          data?.backupCycleDays === undefined || data?.backupCycleDays === null
+            ? ''
+            : String(data.backupCycleDays),
+        memberExpireRemindDays:
+          data?.memberExpireRemindDays === undefined || data?.memberExpireRemindDays === null
+            ? ''
+            : String(data.memberExpireRemindDays),
+      });
+    } catch (error) {
+      message.error((error as Error)?.message || '系统设置加载失败');
+      form.resetFields();
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!canView) {
+      history.replace('/Welcome');
+      return;
+    }
+    loadCurrent();
+  }, [canView]);
+
+  const handleSubmit = async () => {
+    if (!canEdit || submitting) {
+      return;
+    }
+    const values = await form.validateFields();
+    setSubmitting(true);
+    try {
+      await saveCurrentSystemSettings({
+        servicePhone: values.servicePhone?.trim(),
+        serviceOnlineTime: values.serviceOnlineTime?.trim(),
+        backupCycleDays: Number(values.backupCycleDays),
+        memberExpireRemindDays: Number(values.memberExpireRemindDays),
+      });
+      message.success('保存成功');
+      await loadCurrent();
+    } catch (error) {
+      message.error((error as Error)?.message || '保存失败,请稍后重试');
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="system-basic-settings-page">
+      <div className="system-basic-settings-card">
+        <div className="system-basic-settings-card__title">系统设置</div>
+        <Spin spinning={loading}>
+          <Form
+            form={form}
+            layout="horizontal"
+            className="system-basic-settings-form"
+            disabled={loading || !canEdit}
+            labelCol={{ flex: '92px' }}
+            wrapperCol={{ flex: '320px' }}
+          >
+            <div className="system-basic-settings-form__content">
+              <div className="system-basic-settings-form__section">
+                <div className="system-basic-settings-form__section-title">客服设置</div>
+                <div className="system-basic-settings-form__fields">
+                  <Form.Item
+                    label="客服电话"
+                    name="servicePhone"
+                    rules={[
+                      { required: true, message: '请输入客服电话' },
+                      { max: 32, message: '客服电话长度不能超过32个字符' },
+                    ]}
+                  >
+                    <Input
+                      placeholder="请输入"
+                      maxLength={32}
+                      className="system-basic-settings-form__control"
+                    />
+                  </Form.Item>
+                  <Form.Item
+                    label="客服在线时间"
+                    name="serviceOnlineTime"
+                    rules={[
+                      { required: true, message: '请输入客服在线时间' },
+                      { max: 64, message: '客服在线时间长度不能超过64个字符' },
+                    ]}
+                  >
+                    <Input
+                      placeholder="请输入"
+                      maxLength={64}
+                      className="system-basic-settings-form__control"
+                    />
+                  </Form.Item>
+                </div>
+              </div>
+
+              <div className="system-basic-settings-form__section">
+                <div className="system-basic-settings-form__section-title">自动备份设置</div>
+                <div className="system-basic-settings-form__fields">
+                  <Form.Item label="备份周期" required className="system-basic-settings-form__inline-item">
+                    <div className="system-basic-settings-form__inline">
+                      <Form.Item
+                        name="backupCycleDays"
+                        noStyle
+                        rules={[
+                          { required: true, message: '请输入备份周期' },
+                          { validator: validatePositiveInteger('备份周期') },
+                        ]}
+                      >
+                        <Input
+                          placeholder="请输入"
+                          maxLength={9}
+                          className="system-basic-settings-form__inline-input"
+                        />
+                      </Form.Item>
+                      <span className="system-basic-settings-form__inline-unit">天</span>
+                    </div>
+                  </Form.Item>
+                </div>
+              </div>
+
+              <div className="system-basic-settings-form__section">
+                <div className="system-basic-settings-form__section-title">会员提醒设置</div>
+                <div className="system-basic-settings-form__fields">
+                  <Form.Item label="会员到期提醒" required className="system-basic-settings-form__inline-item">
+                    <div className="system-basic-settings-form__inline">
+                      <span className="system-basic-settings-form__inline-prefix">前</span>
+                      <Form.Item
+                        name="memberExpireRemindDays"
+                        noStyle
+                        rules={[
+                          { required: true, message: '请输入会员到期提醒天数' },
+                          { validator: validatePositiveInteger('会员到期提醒天数') },
+                        ]}
+                      >
+                        <Input
+                          placeholder="请输入"
+                          maxLength={9}
+                          className="system-basic-settings-form__inline-input"
+                        />
+                      </Form.Item>
+                      <span className="system-basic-settings-form__inline-unit">天</span>
+                    </div>
+                  </Form.Item>
+                </div>
+              </div>
+            </div>
+
+            <div className="system-basic-settings-form__footer">
+              <Button type="primary" disabled={!canEdit} loading={submitting} onClick={handleSubmit}>
+                保存
+              </Button>
+            </div>
+          </Form>
+        </Spin>
+      </div>
+    </div>
+  );
+};
+
+export default SystemSettingsPage;
diff --git a/tcm-training-admin/src/pages/system/basic-settings/service.ts b/tcm-training-admin/src/pages/system/basic-settings/service.ts
new file mode 100644
index 0000000..2388140
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/basic-settings/service.ts
@@ -0,0 +1,14 @@
+import { request } from '@umijs/max';
+
+export async function getCurrentSystemSettings() {
+  return request('/system-settings/current', {
+    method: 'GET',
+  });
+}
+
+export async function saveCurrentSystemSettings(data: Record<string, any>) {
+  return request('/system-settings/current', {
+    method: 'PUT',
+    data,
+  });
+}
diff --git a/tcm-training-admin/src/pages/system/basic-settings/style.less b/tcm-training-admin/src/pages/system/basic-settings/style.less
new file mode 100644
index 0000000..f178838
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/basic-settings/style.less
@@ -0,0 +1,129 @@
+.system-basic-settings-page {
+  min-height: calc(100vh - 48px);
+  padding: 18px 20px 24px;
+  background: #f2f5f9;
+}
+
+.system-basic-settings-card {
+  min-height: 700px;
+  padding: 8px 14px 18px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.system-basic-settings-card__title {
+  margin: 0 0 22px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.system-basic-settings-form {
+  display: flex;
+  flex-direction: column;
+  min-height: 620px;
+}
+
+.system-basic-settings-form__content {
+  width: 100%;
+}
+
+.system-basic-settings-form__section {
+  margin-bottom: 40px;
+}
+
+.system-basic-settings-form__section-title {
+  position: relative;
+  margin-bottom: 16px;
+  padding-left: 18px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 16px;
+}
+
+.system-basic-settings-form__section-title::before {
+  position: absolute;
+  top: 4px;
+  left: 4px;
+  width: 4px;
+  height: 16px;
+  border-radius: 2px;
+  background: #2f7cff;
+  content: '';
+}
+
+.system-basic-settings-form__fields {
+  width: 100%;
+}
+
+.system-basic-settings-form .ant-form-item {
+  margin-bottom: 18px;
+}
+
+.system-basic-settings-form .ant-form-item-label > label {
+  color: #606266;
+}
+
+.system-basic-settings-form__control {
+  width: 180px;
+}
+
+.system-basic-settings-form__inline-item .ant-form-item-control-input {
+  min-height: 32px;
+}
+
+.system-basic-settings-form__inline {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.system-basic-settings-form__inline-input {
+  width: 122px;
+}
+
+.system-basic-settings-form__inline-prefix,
+.system-basic-settings-form__inline-unit {
+  color: #606266;
+  font-size: 14px;
+}
+
+.system-basic-settings-form__footer {
+  display: flex;
+  justify-content: flex-start;
+  margin-top: auto;
+  padding-top: 220px;
+}
+
+@media (max-width: 900px) {
+  .system-basic-settings-page {
+    padding: 12px;
+  }
+
+  .system-basic-settings-card {
+    min-height: auto;
+    padding: 16px;
+  }
+
+  .system-basic-settings-form {
+    min-height: auto;
+  }
+
+  .system-basic-settings-form__section {
+    margin-bottom: 28px;
+  }
+
+  .system-basic-settings-form__control,
+  .system-basic-settings-form__inline-input {
+    width: 100%;
+  }
+
+  .system-basic-settings-form__inline {
+    width: 100%;
+  }
+
+  .system-basic-settings-form__footer {
+    padding-top: 40px;
+  }
+}
diff --git a/tcm-training-admin/src/pages/system/message-push-settings/index.tsx b/tcm-training-admin/src/pages/system/message-push-settings/index.tsx
new file mode 100644
index 0000000..bb7001e
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/message-push-settings/index.tsx
@@ -0,0 +1,151 @@
+import { history, useAccess } from '@umijs/max';
+import { Button, Checkbox, Spin, Table, message } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import React, { useEffect, useState } from 'react';
+import { getMessagePushSettings, saveMessagePushSettings } from './service';
+import './style.less';
+
+type MessagePushItem = {
+  id: number;
+  noticeTypeCode: string;
+  noticeTypeName: string;
+  pushEnabled: boolean;
+  smsEnabled: boolean;
+  updatedAt?: string;
+};
+
+const MessagePushSettingsPage: React.FC = () => {
+  const access = useAccess();
+  const [loading, setLoading] = useState(false);
+  const [submitting, setSubmitting] = useState(false);
+  const [list, setList] = useState<MessagePushItem[]>([]);
+
+  const canView = access['system:message-push:view'];
+  const canEdit = access['system:message-push:edit'];
+
+  const loadCurrent = async () => {
+    setLoading(true);
+    try {
+      const res = await getMessagePushSettings();
+      setList(res.data || []);
+    } catch (error) {
+      message.error((error as Error)?.message || '消息推送配置加载失败');
+      setList([]);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    if (!canView) {
+      history.replace('/Welcome');
+      return;
+    }
+    loadCurrent();
+  }, [canView]);
+
+  const handleCheckedChange = (
+    noticeTypeCode: string,
+    field: 'pushEnabled' | 'smsEnabled',
+    checked: boolean,
+  ) => {
+    if (!canEdit) {
+      return;
+    }
+    setList((prev) =>
+      prev.map((item) => (item.noticeTypeCode === noticeTypeCode ? { ...item, [field]: checked } : item)),
+    );
+  };
+
+  const handleSubmit = async () => {
+    if (!canEdit || submitting) {
+      return;
+    }
+    setSubmitting(true);
+    try {
+      await saveMessagePushSettings({
+        items: list.map((item) => ({
+          noticeTypeCode: item.noticeTypeCode,
+          pushEnabled: item.pushEnabled,
+          smsEnabled: item.smsEnabled,
+        })),
+      });
+      message.success('保存成功');
+      await loadCurrent();
+    } catch (error) {
+      message.error((error as Error)?.message || '保存失败,请稍后重试');
+    } finally {
+      setSubmitting(false);
+    }
+  };
+
+  const columns: ColumnsType<MessagePushItem> = [
+    {
+      title: '序号',
+      width: 72,
+      render: (_, __, index) => index + 1,
+    },
+    {
+      title: '通知类型',
+      dataIndex: 'noticeTypeName',
+      width: 220,
+    },
+    {
+      title: '通知方式',
+      dataIndex: 'noticeTypeCode',
+      render: (_, record) => (
+        <div className="message-push-settings-table__methods">
+          <Checkbox
+            checked={record.pushEnabled}
+            disabled={loading || submitting || !canEdit}
+            onChange={(event) =>
+              handleCheckedChange(record.noticeTypeCode, 'pushEnabled', event.target.checked)
+            }
+          >
+            PUSH推送
+          </Checkbox>
+          <Checkbox
+            checked={record.smsEnabled}
+            disabled={loading || submitting || !canEdit}
+            onChange={(event) =>
+              handleCheckedChange(record.noticeTypeCode, 'smsEnabled', event.target.checked)
+            }
+          >
+            短信通知
+          </Checkbox>
+        </div>
+      ),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'updatedAt',
+      width: 180,
+    },
+  ];
+
+  return (
+    <div className="message-push-settings-page">
+      <div className="message-push-settings-card">
+        <div className="message-push-settings-card__title">消息推送配置</div>
+        <Spin spinning={loading}>
+          <div className="message-push-settings-table-card">
+            <Table<MessagePushItem>
+              rowKey="noticeTypeCode"
+              columns={columns}
+              dataSource={list}
+              pagination={false}
+              scroll={{ x: 900 }}
+            />
+          </div>
+          <div className="message-push-settings-footer">
+            <Button type="primary" disabled={!canEdit} loading={submitting} onClick={handleSubmit}>
+              保存
+            </Button>
+          </div>
+        </Spin>
+      </div>
+    </div>
+  );
+};
+
+export default MessagePushSettingsPage;
diff --git a/tcm-training-admin/src/pages/system/message-push-settings/service.ts b/tcm-training-admin/src/pages/system/message-push-settings/service.ts
new file mode 100644
index 0000000..906a767
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/message-push-settings/service.ts
@@ -0,0 +1,14 @@
+import { request } from '@umijs/max';
+
+export async function getMessagePushSettings() {
+  return request('/message-push-settings', {
+    method: 'GET',
+  });
+}
+
+export async function saveMessagePushSettings(data: Record<string, any>) {
+  return request('/message-push-settings', {
+    method: 'PUT',
+    data,
+  });
+}
diff --git a/tcm-training-admin/src/pages/system/message-push-settings/style.less b/tcm-training-admin/src/pages/system/message-push-settings/style.less
new file mode 100644
index 0000000..1a0eb9e
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/message-push-settings/style.less
@@ -0,0 +1,72 @@
+.message-push-settings-page {
+  min-height: calc(100vh - 48px);
+  padding: 18px 20px 24px;
+  background: #f2f5f9;
+}
+
+.message-push-settings-card {
+  min-height: 700px;
+  padding: 8px 14px 18px;
+  background: #fff;
+  border-radius: 10px;
+  box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
+}
+
+.message-push-settings-card__title {
+  margin: 0 0 22px;
+  color: #303133;
+  font-weight: 500;
+  font-size: 18px;
+}
+
+.message-push-settings-table-card {
+  overflow: hidden;
+  border: 1px solid #e6ebf2;
+  border-radius: 4px;
+}
+
+.message-push-settings-table-card .ant-table-thead > tr > th {
+  color: #303133;
+  font-weight: 500;
+  background: #f5f7fa;
+}
+
+.message-push-settings-table-card .ant-table-tbody > tr > td {
+  color: #606266;
+}
+
+.message-push-settings-table__methods {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px 48px;
+  align-items: center;
+}
+
+.message-push-settings-table__methods .ant-checkbox-wrapper {
+  color: #606266;
+}
+
+.message-push-settings-footer {
+  display: flex;
+  justify-content: flex-start;
+  padding-top: 160px;
+}
+
+@media (max-width: 900px) {
+  .message-push-settings-page {
+    padding: 12px;
+  }
+
+  .message-push-settings-card {
+    min-height: auto;
+    padding: 16px;
+  }
+
+  .message-push-settings-table__methods {
+    gap: 12px 24px;
+  }
+
+  .message-push-settings-footer {
+    padding-top: 40px;
+  }
+}
diff --git a/tcm-training-admin/src/pages/system/settings-redirect/index.tsx b/tcm-training-admin/src/pages/system/settings-redirect/index.tsx
new file mode 100644
index 0000000..4dc9c32
--- /dev/null
+++ b/tcm-training-admin/src/pages/system/settings-redirect/index.tsx
@@ -0,0 +1,34 @@
+import { history, useAccess } from '@umijs/max';
+import React, { useEffect } from 'react';
+
+const SystemSettingsRedirectPage: React.FC = () => {
+  const access = useAccess();
+
+  useEffect(() => {
+    if (access['system:basic-settings:view']) {
+      history.replace('/system/settings/basic');
+      return;
+    }
+    if (access['system:app-version:view']) {
+      history.replace('/system/settings/app-version');
+      return;
+    }
+    if (access['system:banner:view']) {
+      history.replace('/system/settings/banner');
+      return;
+    }
+    if (access['system:message-push:view']) {
+      history.replace('/system/settings/message-push');
+      return;
+    }
+    if (access['system:points-settings:view']) {
+      history.replace('/system/settings/points');
+      return;
+    }
+    history.replace('/Welcome');
+  }, [access]);
+
+  return null;
+};
+
+export default SystemSettingsRedirectPage;

--
Gitblit v1.7.1