From 5ad8d714d43ecf06aa9d900a69e7b22018ce7ed6 Mon Sep 17 00:00:00 2001
From: “mitao” <“2763622819@qq.com”>
Date: 星期三, 15 四月 2026 23:09:44 +0800
Subject: [PATCH] feat:APP个人中心

---
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseMemberJoinRecordMapper.java           |    8 
 tcm-training-admin-backend/src/main/resources/mapper/PointsLedgerMapper.xml                                                   |   12 
 traditionalApp/pages/courseDetail/courseDetail.vue                                                                            |  123 
 traditionalApp/pages/myCourseDetail/myCourseDetail.vue                                                                        | 1085 ++++++
 traditionalApp/pages/point/service.js                                                                                         |    8 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseFavoriteBatchCancelRequest.java |   21 
 tcm-training-admin-backend/src/main/resources/mapper/HealthCourseFavoriteRecordMapper.xml                                     |   38 
 tcm-training-admin-backend/src/main/resources/mapper/HealthCourseDownloadRecordMapper.xml                                     |   23 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCoursePlayRecordUploadRequest.java    |   32 
 traditionalApp/pages/courseRecordDetail/courseRecordDetail.vue                                                                |  500 ++-
 traditionalApp/pages/courseRecordDetail/service.js                                                                            |    8 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/service/AppUserCenterService.java                    |  154 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsLedgerItemVO.java                      |   20 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseFavoriteRecordMapper.java             |    6 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/CoursePurchaseOrderAppService.java               |  122 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCoursePlayRecord.java                       |   48 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/mapper/PointsLedgerMapper.java                         |    4 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsLedgerController.java          |   40 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppHealthCourseController.java          |   87 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseItemVO.java                    |   22 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java                |  732 ++++
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/vo/AppUserCenterVO.java                              |   25 
 tcm-training-admin-backend/src/main/resources/mapper/CoursePurchaseOrderMapper.xml                                            |  104 
 traditionalApp/pages/courseRecord/courseRecord.vue                                                                            |  602 ++-
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseFavoriteRow.java                      |   26 
 traditionalApp/pages/favorites/service.js                                                                                     |   22 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/vo/AppMemberPurchaseOrderItemVO.java               |   27 
 traditionalApp/pages/pointDetail/service.js                                                                                   |    8 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCoursePlayRecordUploadResultVO.java    |   19 
 traditionalApp/pages/mine/service.js                                                                                          |   19 
 tcm-training-admin-backend/src/main/resources/mapper/HealthCourseMemberJoinRecordMapper.xml                                   |   14 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/controller/AppMemberOpenPageController.java        |   18 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsOverviewVO.java                        |   25 
 traditionalApp/pages/courseDetail/service.js                                                                                  |    7 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderDetailVO.java             |   33 
 traditionalApp/pages/mine/mine.vue                                                                                            |  780 +++-
 tcm-training-admin-backend/sql/tcm_training_admin.sql                                                                         |   71 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadBatchDeleteRequest.java |   21 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseProgressRow.java                    |   18 
 tcm-training-admin-backend/src/main/resources/mapper/HealthCoursePlayRecordMapper.xml                                         |   52 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppCoursePurchaseOrderController.java   |   50 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/MemberPurchaseOrderAppService.java             |   84 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadReportRequest.java      |   22 
 traditionalApp/pages/pointDetail/pointDetail.vue                                                                              |  268 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseVideoProgressRow.java               |   21 
 traditionalApp/pages/vipRecord/service.js                                                                                     |    8 
 tcm-training-admin-backend/sql/init.sql                                                                                       |   72 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseDetailVO.java                  |   25 
 tcm-training-admin-backend/sql/update_app_my_course_schema.sql                                                                |   87 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsLedgerService.java                |  113 
 tcm-training-admin-backend/src/main/resources/mapper/MemberPurchaseOrderMapper.xml                                            |   23 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/CoursePurchaseOrderListRow.java                       |    7 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCourseDownloadRecord.java                   |   65 
 traditionalApp/pages/myCourses/myCourses.vue                                                                                  |  361 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseVideoItemVO.java               |   22 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/CoursePurchaseOrderMapper.java                    |    9 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseDownloadRow.java                      |   24 
 traditionalApp/pages/point/point.vue                                                                                          |  184 +
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/MemberPurchaseOrderMapper.java                  |    3 
 traditionalApp/pages/downloads/service.js                                                                                     |   15 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderItemVO.java               |   30 
 traditionalApp/pages/vipRecord/vipRecord.vue                                                                                  |  421 +-
 traditionalApp/pages/courseRecord/service.js                                                                                  |    8 
 traditionalApp/pages/favorites/favorites.vue                                                                                  |  833 +++--
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseDownloadItemVO.java              |   25 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsOverviewService.java              |  138 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseDownloadRecordMapper.java             |   21 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCoursePlayRecordMapper.java                 |   29 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseFavoriteItemVO.java              |   24 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserCenterController.java              |   35 
 traditionalApp/pages/downloads/downloads.vue                                                                                  |  965 ++++--
 traditionalApp/utils/download-task-storage.js                                                                                 |  142 
 tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsOverviewController.java        |   35 
 traditionalApp/pages/myCourses/service.js                                                                                     |   15 
 traditionalApp/pages/myCourseDetail/service.js                                                                                |   15 
 75 files changed, 7,261 insertions(+), 1,922 deletions(-)

diff --git a/tcm-training-admin-backend/sql/init.sql b/tcm-training-admin-backend/sql/init.sql
index f99ebc8..2984d09 100644
--- a/tcm-training-admin-backend/sql/init.sql
+++ b/tcm-training-admin-backend/sql/init.sql
@@ -646,6 +646,7 @@
   `play_end` DATETIME DEFAULT NULL COMMENT '结束播放时间',
   `duration_seconds` INT NOT NULL DEFAULT 0 COMMENT '累计播放时长(秒)',
   `is_completed` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否完播:1是 0否',
+  `last_play_position_seconds` INT NOT NULL DEFAULT 0 COMMENT '本次结束时播放位置(秒)',
   PRIMARY KEY (`id`),
   KEY `idx_health_course_play_course` (`course_id`),
   KEY `idx_health_course_play_video` (`video_id`),
@@ -667,6 +668,29 @@
   KEY `idx_health_course_favorite_user` (`app_user_id`),
   KEY `idx_health_course_favorite_time` (`action_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='养生课程收藏记录表';
+
+-- ----------------------------
+-- 养生课程:下载记录
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS `tcm_health_course_download_record` (
+  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `app_user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
+  `course_id` BIGINT UNSIGNED NOT NULL COMMENT '养生课程ID',
+  `course_video_id` BIGINT UNSIGNED NOT NULL COMMENT '视频小节ID',
+  `course_title_snapshot` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '课程标题快照',
+  `chapter_name_snapshot` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '章节标题快照',
+  `cover_img_snapshot` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '封面图快照',
+  `video_url_snapshot` VARCHAR(512) NOT NULL DEFAULT '' COMMENT '视频地址快照',
+  `duration_seconds_snapshot` INT NOT NULL DEFAULT 0 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_health_course_download_user_deleted_created` (`app_user_id`, `deleted`, `created_at`),
+  KEY `idx_health_course_download_user_video_deleted` (`app_user_id`, `course_video_id`, `deleted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='养生课程下载记录表';
 
 -- ----------------------------
 -- 养生课程:会员加课记录
@@ -963,31 +987,31 @@
 (5034, 1, 107, 200, TIMESTAMP(CURDATE(), '20:40:00'), TIMESTAMP(CURDATE(), '20:46:00'), 360, 0, 1);
 
 INSERT IGNORE INTO `tcm_health_course_play_record`
-(`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`)
+(`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`)
 VALUES
-(5101, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '10:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '10:18:00'), 1080, 1),
-(5102, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '15:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '15:18:00'), 480, 0),
-(5103, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '10:02:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '10:20:00'), 1080, 1),
-(5104, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '14:30:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '14:38:00'), 480, 0),
-(5105, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '09:55:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '10:13:00'), 1080, 1),
-(5106, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '13:20:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '13:28:00'), 480, 0),
-(5107, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '19:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '19:18:00'), 480, 0),
-(5108, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '10:05:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '10:23:00'), 1080, 1),
-(5109, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '12:40:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '12:48:00'), 480, 0),
-(5110, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '18:25:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '18:33:00'), 480, 0),
-(5111, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '10:08:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '10:26:00'), 1080, 1),
-(5112, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '13:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '13:18:00'), 480, 0),
-(5113, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '18:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '18:08:00'), 480, 0),
-(5114, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '20:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '20:18:00'), 480, 0),
-(5115, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '09:58:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '10:16:00'), 1080, 1),
-(5116, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '12:20:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '12:28:00'), 480, 0),
-(5117, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '17:42:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '17:50:00'), 480, 0),
-(5118, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '21:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '21:08:00'), 480, 0),
-(5119, 1, 1, 200, TIMESTAMP(CURDATE(), '09:50:00'), TIMESTAMP(CURDATE(), '10:08:00'), 1080, 1),
-(5120, 1, 2, 200, TIMESTAMP(CURDATE(), '12:15:00'), TIMESTAMP(CURDATE(), '12:23:00'), 480, 0),
-(5121, 1, 3, 200, TIMESTAMP(CURDATE(), '15:30:00'), TIMESTAMP(CURDATE(), '15:38:00'), 480, 0),
-(5122, 1, 1, 200, TIMESTAMP(CURDATE(), '18:20:00'), TIMESTAMP(CURDATE(), '18:28:00'), 480, 0),
-(5123, 1, 2, 200, TIMESTAMP(CURDATE(), '21:10:00'), TIMESTAMP(CURDATE(), '21:18:00'), 480, 0);
+(5101, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '10:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '10:18:00'), 1080, 1, 0),
+(5102, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '15:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 6 DAY), '15:18:00'), 480, 0, 480),
+(5103, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '10:02:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '10:20:00'), 1080, 1, 0),
+(5104, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '14:30:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 5 DAY), '14:38:00'), 480, 0, 480),
+(5105, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '09:55:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '10:13:00'), 1080, 1, 0),
+(5106, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '13:20:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '13:28:00'), 480, 0, 480),
+(5107, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '19:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 4 DAY), '19:18:00'), 480, 0, 480),
+(5108, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '10:05:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '10:23:00'), 1080, 1, 0),
+(5109, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '12:40:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '12:48:00'), 480, 0, 480),
+(5110, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '18:25:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 3 DAY), '18:33:00'), 480, 0, 480),
+(5111, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '10:08:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '10:26:00'), 1080, 1, 0),
+(5112, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '13:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '13:18:00'), 480, 0, 480),
+(5113, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '18:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '18:08:00'), 480, 0, 480),
+(5114, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '20:10:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 2 DAY), '20:18:00'), 480, 0, 480),
+(5115, 1, 1, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '09:58:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '10:16:00'), 1080, 1, 0),
+(5116, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '12:20:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '12:28:00'), 480, 0, 480),
+(5117, 1, 3, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '17:42:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '17:50:00'), 480, 0, 480),
+(5118, 1, 2, 200, TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '21:00:00'), TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL 1 DAY), '21:08:00'), 480, 0, 480),
+(5119, 1, 1, 200, TIMESTAMP(CURDATE(), '09:50:00'), TIMESTAMP(CURDATE(), '10:08:00'), 1080, 1, 0),
+(5120, 1, 2, 200, TIMESTAMP(CURDATE(), '12:15:00'), TIMESTAMP(CURDATE(), '12:23:00'), 480, 0, 480),
+(5121, 1, 3, 200, TIMESTAMP(CURDATE(), '15:30:00'), TIMESTAMP(CURDATE(), '15:38:00'), 480, 0, 480),
+(5122, 1, 1, 200, TIMESTAMP(CURDATE(), '18:20:00'), TIMESTAMP(CURDATE(), '18:28:00'), 480, 0, 480),
+(5123, 1, 2, 200, TIMESTAMP(CURDATE(), '21:10:00'), TIMESTAMP(CURDATE(), '21:18:00'), 480, 0, 480);
 
 INSERT IGNORE INTO `tcm_health_course_favorite_record`
 (`id`, `course_id`, `app_user_id`, `action_type`, `action_time`)
diff --git a/tcm-training-admin-backend/sql/tcm_training_admin.sql b/tcm-training-admin-backend/sql/tcm_training_admin.sql
index 45a4fb3..0accdb4 100644
--- a/tcm-training-admin-backend/sql/tcm_training_admin.sql
+++ b/tcm-training-admin-backend/sql/tcm_training_admin.sql
@@ -4457,6 +4457,30 @@
 COMMIT;
 
 -- ----------------------------
+-- Table structure for tcm_health_course_download_record
+-- ----------------------------
+DROP TABLE IF EXISTS `tcm_health_course_download_record`;
+CREATE TABLE `tcm_health_course_download_record` (
+  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+  `app_user_id` bigint unsigned NOT NULL COMMENT '用户ID',
+  `course_id` bigint unsigned NOT NULL COMMENT '养生课程ID',
+  `course_video_id` bigint unsigned NOT NULL COMMENT '视频小节ID',
+  `course_title_snapshot` varchar(128) NOT NULL DEFAULT '' COMMENT '课程标题快照',
+  `chapter_name_snapshot` varchar(128) NOT NULL DEFAULT '' COMMENT '章节标题快照',
+  `cover_img_snapshot` varchar(255) NOT NULL DEFAULT '' COMMENT '封面图快照',
+  `video_url_snapshot` varchar(512) NOT NULL DEFAULT '' COMMENT '视频地址快照',
+  `duration_seconds_snapshot` int NOT NULL DEFAULT '0' 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_health_course_download_user_deleted_created` (`app_user_id`,`deleted`,`created_at`),
+  KEY `idx_health_course_download_user_video_deleted` (`app_user_id`,`course_video_id`,`deleted`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='养生课程下载记录表';
+
+-- ----------------------------
 -- Table structure for tcm_health_course_favorite_record
 -- ----------------------------
 DROP TABLE IF EXISTS `tcm_health_course_favorite_record`;
@@ -4510,6 +4534,7 @@
   `play_end` datetime DEFAULT NULL COMMENT '结束播放时间',
   `duration_seconds` int NOT NULL DEFAULT '0' COMMENT '累计播放时长(秒)',
   `is_completed` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否完播:1是 0否',
+  `last_play_position_seconds` int NOT NULL DEFAULT '0' COMMENT '本次结束时播放位置(秒)',
   PRIMARY KEY (`id`),
   KEY `idx_health_course_play_course` (`course_id`),
   KEY `idx_health_course_play_video` (`video_id`),
@@ -4521,29 +4546,29 @@
 -- Records of tcm_health_course_play_record
 -- ----------------------------
 BEGIN;
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5101, 1, 1, 200, '2026-03-20 10:00:00', '2026-03-20 10:18:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5102, 1, 2, 200, '2026-03-20 15:10:00', '2026-03-20 15:18:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5103, 1, 1, 200, '2026-03-21 10:02:00', '2026-03-21 10:20:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5104, 1, 3, 200, '2026-03-21 14:30:00', '2026-03-21 14:38:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5105, 1, 1, 200, '2026-03-22 09:55:00', '2026-03-22 10:13:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5106, 1, 2, 200, '2026-03-22 13:20:00', '2026-03-22 13:28:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5107, 1, 3, 200, '2026-03-22 19:10:00', '2026-03-22 19:18:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5108, 1, 1, 200, '2026-03-23 10:05:00', '2026-03-23 10:23:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5109, 1, 2, 200, '2026-03-23 12:40:00', '2026-03-23 12:48:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5110, 1, 3, 200, '2026-03-23 18:25:00', '2026-03-23 18:33:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5111, 1, 1, 200, '2026-03-24 10:08:00', '2026-03-24 10:26:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5112, 1, 2, 200, '2026-03-24 13:10:00', '2026-03-24 13:18:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5113, 1, 3, 200, '2026-03-24 18:00:00', '2026-03-24 18:08:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5114, 1, 1, 200, '2026-03-24 20:10:00', '2026-03-24 20:18:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5115, 1, 1, 200, '2026-03-25 09:58:00', '2026-03-25 10:16:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5116, 1, 2, 200, '2026-03-25 12:20:00', '2026-03-25 12:28:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5117, 1, 3, 200, '2026-03-25 17:42:00', '2026-03-25 17:50:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5118, 1, 2, 200, '2026-03-25 21:00:00', '2026-03-25 21:08:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5119, 1, 1, 200, '2026-03-26 09:50:00', '2026-03-26 10:08:00', 1080, 1);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5120, 1, 2, 200, '2026-03-26 12:15:00', '2026-03-26 12:23:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5121, 1, 3, 200, '2026-03-26 15:30:00', '2026-03-26 15:38:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5122, 1, 1, 200, '2026-03-26 18:20:00', '2026-03-26 18:28:00', 480, 0);
-INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`) VALUES (5123, 1, 2, 200, '2026-03-26 21:10:00', '2026-03-26 21:18:00', 480, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5101, 1, 1, 200, '2026-03-20 10:00:00', '2026-03-20 10:18:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5102, 1, 2, 200, '2026-03-20 15:10:00', '2026-03-20 15:18:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5103, 1, 1, 200, '2026-03-21 10:02:00', '2026-03-21 10:20:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5104, 1, 3, 200, '2026-03-21 14:30:00', '2026-03-21 14:38:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5105, 1, 1, 200, '2026-03-22 09:55:00', '2026-03-22 10:13:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5106, 1, 2, 200, '2026-03-22 13:20:00', '2026-03-22 13:28:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5107, 1, 3, 200, '2026-03-22 19:10:00', '2026-03-22 19:18:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5108, 1, 1, 200, '2026-03-23 10:05:00', '2026-03-23 10:23:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5109, 1, 2, 200, '2026-03-23 12:40:00', '2026-03-23 12:48:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5110, 1, 3, 200, '2026-03-23 18:25:00', '2026-03-23 18:33:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5111, 1, 1, 200, '2026-03-24 10:08:00', '2026-03-24 10:26:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5112, 1, 2, 200, '2026-03-24 13:10:00', '2026-03-24 13:18:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5113, 1, 3, 200, '2026-03-24 18:00:00', '2026-03-24 18:08:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5114, 1, 1, 200, '2026-03-24 20:10:00', '2026-03-24 20:18:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5115, 1, 1, 200, '2026-03-25 09:58:00', '2026-03-25 10:16:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5116, 1, 2, 200, '2026-03-25 12:20:00', '2026-03-25 12:28:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5117, 1, 3, 200, '2026-03-25 17:42:00', '2026-03-25 17:50:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5118, 1, 2, 200, '2026-03-25 21:00:00', '2026-03-25 21:08:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5119, 1, 1, 200, '2026-03-26 09:50:00', '2026-03-26 10:08:00', 1080, 1, 0);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5120, 1, 2, 200, '2026-03-26 12:15:00', '2026-03-26 12:23:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5121, 1, 3, 200, '2026-03-26 15:30:00', '2026-03-26 15:38:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5122, 1, 1, 200, '2026-03-26 18:20:00', '2026-03-26 18:28:00', 480, 0, 480);
+INSERT INTO `tcm_health_course_play_record` (`id`, `course_id`, `video_id`, `app_user_id`, `play_start`, `play_end`, `duration_seconds`, `is_completed`, `last_play_position_seconds`) VALUES (5123, 1, 2, 200, '2026-03-26 21:10:00', '2026-03-26 21:18:00', 480, 0, 480);
 COMMIT;
 
 -- ----------------------------
diff --git a/tcm-training-admin-backend/sql/update_app_my_course_schema.sql b/tcm-training-admin-backend/sql/update_app_my_course_schema.sql
new file mode 100644
index 0000000..062a9b4
--- /dev/null
+++ b/tcm-training-admin-backend/sql/update_app_my_course_schema.sql
@@ -0,0 +1,87 @@
+SET @db_name = DATABASE();
+
+-- ----------------------------
+-- APP 我的课程相关表结构升级
+-- 目标:
+-- 1. 养生课程播放记录表新增续播位置字段 last_play_position_seconds
+-- 2. 新增养生课程下载记录表 tcm_health_course_download_record
+-- ----------------------------
+
+SET @play_record_table_exists = (
+  SELECT COUNT(1)
+  FROM information_schema.TABLES
+  WHERE TABLE_SCHEMA = @db_name
+    AND TABLE_NAME = 'tcm_health_course_play_record'
+);
+
+SET @sql_assert_play_record_table = IF(
+  @play_record_table_exists = 0,
+  'SELECT ''skip: tcm_health_course_play_record not exists''',
+  'SELECT 1'
+);
+PREPARE stmt_assert_play_record_table FROM @sql_assert_play_record_table;
+EXECUTE stmt_assert_play_record_table;
+DEALLOCATE PREPARE stmt_assert_play_record_table;
+
+SET @has_last_play_position_seconds = (
+  SELECT COUNT(1)
+  FROM information_schema.COLUMNS
+  WHERE TABLE_SCHEMA = @db_name
+    AND TABLE_NAME = 'tcm_health_course_play_record'
+    AND COLUMN_NAME = 'last_play_position_seconds'
+);
+
+SET @sql_add_last_play_position_seconds = IF(
+  @play_record_table_exists = 1 AND @has_last_play_position_seconds = 0,
+  'ALTER TABLE `tcm_health_course_play_record`
+   ADD COLUMN `last_play_position_seconds` INT NOT NULL DEFAULT 0 COMMENT ''本次结束时播放位置(秒)'' AFTER `is_completed`',
+  'SELECT 1'
+);
+PREPARE stmt_add_last_play_position_seconds FROM @sql_add_last_play_position_seconds;
+EXECUTE stmt_add_last_play_position_seconds;
+DEALLOCATE PREPARE stmt_add_last_play_position_seconds;
+
+SET @sql_fill_last_play_position_seconds = IF(
+  @play_record_table_exists = 1,
+  'UPDATE `tcm_health_course_play_record`
+   SET `last_play_position_seconds` = 0
+   WHERE `last_play_position_seconds` IS NULL',
+  'SELECT 1'
+);
+PREPARE stmt_fill_last_play_position_seconds FROM @sql_fill_last_play_position_seconds;
+EXECUTE stmt_fill_last_play_position_seconds;
+DEALLOCATE PREPARE stmt_fill_last_play_position_seconds;
+
+SET @download_record_table_exists = (
+  SELECT COUNT(1)
+  FROM information_schema.TABLES
+  WHERE TABLE_SCHEMA = @db_name
+    AND TABLE_NAME = 'tcm_health_course_download_record'
+);
+
+SET @sql_create_download_record_table = IF(
+  @download_record_table_exists = 0,
+  'CREATE TABLE `tcm_health_course_download_record` (
+      `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+      `app_user_id` BIGINT UNSIGNED NOT NULL COMMENT ''用户ID'',
+      `course_id` BIGINT UNSIGNED NOT NULL COMMENT ''养生课程ID'',
+      `course_video_id` BIGINT UNSIGNED NOT NULL COMMENT ''视频小节ID'',
+      `course_title_snapshot` VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''课程标题快照'',
+      `chapter_name_snapshot` VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''章节标题快照'',
+      `cover_img_snapshot` VARCHAR(255) NOT NULL DEFAULT '''' COMMENT ''封面图快照'',
+      `video_url_snapshot` VARCHAR(512) NOT NULL DEFAULT '''' COMMENT ''视频地址快照'',
+      `duration_seconds_snapshot` INT NOT NULL DEFAULT 0 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_health_course_download_user_deleted_created` (`app_user_id`, `deleted`, `created_at`),
+      KEY `idx_health_course_download_user_video_deleted` (`app_user_id`, `course_video_id`, `deleted`)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''养生课程下载记录表''',
+  'SELECT 1'
+);
+PREPARE stmt_create_download_record_table FROM @sql_create_download_record_table;
+EXECUTE stmt_create_download_record_table;
+DEALLOCATE PREPARE stmt_create_download_record_table;
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppCoursePurchaseOrderController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppCoursePurchaseOrderController.java
new file mode 100644
index 0000000..d892834
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppCoursePurchaseOrderController.java
@@ -0,0 +1,50 @@
+package com.tcm.training.admin.app.course.controller;
+
+import com.tcm.training.admin.app.course.vo.AppCoursePurchaseOrderDetailVO;
+import com.tcm.training.admin.app.course.vo.AppCoursePurchaseOrderItemVO;
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.live.service.CoursePurchaseOrderAppService;
+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.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * APP 购课记录接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/app", "/api/v1/app"})
+public class AppCoursePurchaseOrderController {
+
+    private final CoursePurchaseOrderAppService coursePurchaseOrderAppService;
+
+    /**
+     * 注入 APP 购课记录服务。
+     */
+    public AppCoursePurchaseOrderController(CoursePurchaseOrderAppService coursePurchaseOrderAppService) {
+        this.coursePurchaseOrderAppService = coursePurchaseOrderAppService;
+    }
+
+    /**
+     * 获取当前登录用户的购课记录列表。
+     */
+    @GetMapping("/course-purchase-orders")
+    public ApiResponse<PageResponse<AppCoursePurchaseOrderItemVO>> page(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize,
+            @RequestParam(value = "productName", required = false) String productName) {
+        return ApiResponse.ok(coursePurchaseOrderAppService.appUserPage(page, pageSize, productName));
+    }
+
+    /**
+     * 获取当前登录用户的购课记录详情。
+     */
+    @GetMapping("/course-purchase-orders/{orderNo}")
+    public ApiResponse<AppCoursePurchaseOrderDetailVO> detail(@PathVariable("orderNo") String orderNo) {
+        return ApiResponse.ok(coursePurchaseOrderAppService.appUserDetail(orderNo));
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppHealthCourseController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppHealthCourseController.java
index 7afa6a9..fe7bf53 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppHealthCourseController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/controller/AppHealthCourseController.java
@@ -1,12 +1,21 @@
 package com.tcm.training.admin.app.course.controller;
 
+import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadBatchDeleteRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadReportRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCourseFavoriteBatchCancelRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCoursePlayRecordUploadRequest;
 import com.tcm.training.admin.app.course.dto.AppHealthCoursePurchaseRequest;
+import com.tcm.training.admin.app.course.vo.AppHealthCourseDownloadItemVO;
 import com.tcm.training.admin.app.course.service.AppHealthCourseService;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseCategoryOptionVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO;
+import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteItemVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteResultVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseListItemVO;
+import com.tcm.training.admin.app.course.vo.AppHealthCoursePlayRecordUploadResultVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCoursePurchaseResultVO;
+import com.tcm.training.admin.app.course.vo.AppMyHealthCourseDetailVO;
+import com.tcm.training.admin.app.course.vo.AppMyHealthCourseItemVO;
 import com.tcm.training.admin.common.ApiResponse;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.validation.annotation.Validated;
@@ -56,6 +65,75 @@
     }
 
     /**
+     * 获取当前登录用户的收藏课程列表。
+     */
+    @GetMapping("/health-courses/favorites")
+    public ApiResponse<List<AppHealthCourseFavoriteItemVO>> favorites() {
+        return ApiResponse.ok(appHealthCourseService.favorites());
+    }
+
+    /**
+     * 获取当前登录用户的下载记录列表。
+     */
+    @GetMapping("/health-course-downloads")
+    public ApiResponse<List<AppHealthCourseDownloadItemVO>> downloads() {
+        return ApiResponse.ok(appHealthCourseService.downloads());
+    }
+
+    /**
+     * 获取当前登录用户的我的课程列表。
+     */
+    @GetMapping("/my-health-courses")
+    public ApiResponse<List<AppMyHealthCourseItemVO>> myCourses() {
+        return ApiResponse.ok(appHealthCourseService.myCourses());
+    }
+
+    /**
+     * 删除当前登录用户的会员加课课程。
+     */
+    @DeleteMapping("/my-health-courses/{courseId}")
+    public ApiResponse<Void> removeMyCourse(@PathVariable("courseId") Long courseId) {
+        appHealthCourseService.removeMyCourse(courseId);
+        return ApiResponse.ok(null);
+    }
+
+    /**
+     * 获取当前登录用户的我的课程详情。
+     */
+    @GetMapping("/my-health-courses/{courseId}")
+    public ApiResponse<AppMyHealthCourseDetailVO> myCourseDetail(@PathVariable("courseId") Long courseId) {
+        return ApiResponse.ok(appHealthCourseService.myCourseDetail(courseId));
+    }
+
+    /**
+     * 上报当前登录用户的我的课程播放记录。
+     */
+    @PostMapping("/my-health-courses/{courseId}/videos/{videoId}/play-records")
+    public ApiResponse<AppHealthCoursePlayRecordUploadResultVO> uploadMyCoursePlayRecord(@PathVariable("courseId") Long courseId,
+                                                                                          @PathVariable("videoId") Long videoId,
+                                                                                          @Valid @RequestBody AppHealthCoursePlayRecordUploadRequest request) {
+        return ApiResponse.ok(appHealthCourseService.uploadMyCoursePlayRecord(courseId, videoId, request));
+    }
+
+    /**
+     * 上报当前登录用户完成下载的课程小节。
+     */
+    @PostMapping("/health-course-downloads")
+    public ApiResponse<Void> reportDownload(@Valid @RequestBody AppHealthCourseDownloadReportRequest request) {
+        appHealthCourseService.reportDownload(request);
+        return ApiResponse.ok(null);
+    }
+
+    /**
+     * 批量删除当前登录用户的下载记录。
+     */
+    @DeleteMapping("/health-course-downloads")
+    public ApiResponse<Void> batchDeleteDownloads(@Valid @RequestBody AppHealthCourseDownloadBatchDeleteRequest request) {
+        appHealthCourseService.batchDeleteDownloads(request);
+        return ApiResponse.ok(null);
+    }
+
+    /**
      * 收藏 APP 养生课程。
      */
     @PostMapping("/health-courses/{courseId}/favorite")
@@ -72,6 +150,15 @@
     }
 
     /**
+     * 批量取消 APP 养生课程收藏。
+     */
+    @DeleteMapping("/health-courses/favorites")
+    public ApiResponse<Void> batchCancelFavorite(@Valid @RequestBody AppHealthCourseFavoriteBatchCancelRequest request) {
+        appHealthCourseService.batchCancelFavorite(request);
+        return ApiResponse.ok(null);
+    }
+
+    /**
      * 购买 APP 养生课程整课。
      */
     @PostMapping("/health-courses/{courseId}/purchase")
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadBatchDeleteRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadBatchDeleteRequest.java
new file mode 100644
index 0000000..d288851
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadBatchDeleteRequest.java
@@ -0,0 +1,21 @@
+package com.tcm.training.admin.app.course.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * APP 养生课程批量删除下载记录请求。
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppHealthCourseDownloadBatchDeleteRequest {
+
+    @NotEmpty(message = "请选择要删除的下载记录")
+    private List<@NotNull(message = "下载记录标识不能为空") Long> ids;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadReportRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadReportRequest.java
new file mode 100644
index 0000000..1978613
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseDownloadReportRequest.java
@@ -0,0 +1,22 @@
+package com.tcm.training.admin.app.course.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * APP 养生课程下载完成上报请求。
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppHealthCourseDownloadReportRequest {
+
+    @NotNull(message = "课程标识不能为空")
+    private Long courseId;
+
+    @NotNull(message = "课程小节标识不能为空")
+    private Long courseVideoId;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseFavoriteBatchCancelRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseFavoriteBatchCancelRequest.java
new file mode 100644
index 0000000..29082d7
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCourseFavoriteBatchCancelRequest.java
@@ -0,0 +1,21 @@
+package com.tcm.training.admin.app.course.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * APP 养生课程批量取消收藏请求。
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppHealthCourseFavoriteBatchCancelRequest {
+
+    @NotEmpty(message = "请选择要取消收藏的课程")
+    private List<@NotNull(message = "课程标识不能为空") Long> courseIds;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCoursePlayRecordUploadRequest.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCoursePlayRecordUploadRequest.java
new file mode 100644
index 0000000..d6531b0
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/dto/AppHealthCoursePlayRecordUploadRequest.java
@@ -0,0 +1,32 @@
+package com.tcm.training.admin.app.course.dto;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * APP 养生课程播放记录上报请求。
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppHealthCoursePlayRecordUploadRequest {
+
+    @NotNull(message = "播放开始时间不能为空")
+    private LocalDateTime playStart;
+
+    @NotNull(message = "播放结束时间不能为空")
+    private LocalDateTime playEnd;
+
+    @NotNull(message = "播放时长不能为空")
+    @Min(value = 0, message = "播放时长不能小于0")
+    private Integer durationSeconds;
+
+    @NotNull(message = "播放位置不能为空")
+    @Min(value = 0, message = "播放位置不能小于0")
+    private Integer lastPlayPositionSeconds;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
index 4d4f298..5d4fb5e 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/service/AppHealthCourseService.java
@@ -3,32 +3,51 @@
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadBatchDeleteRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCourseDownloadReportRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCourseFavoriteBatchCancelRequest;
+import com.tcm.training.admin.app.course.dto.AppHealthCoursePlayRecordUploadRequest;
 import com.tcm.training.admin.app.course.dto.AppHealthCoursePurchaseRequest;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseCategoryOptionVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO;
+import com.tcm.training.admin.app.course.vo.AppHealthCourseDownloadItemVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseExpertVO;
+import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteItemVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseFavoriteResultVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseListItemVO;
+import com.tcm.training.admin.app.course.vo.AppHealthCoursePlayRecordUploadResultVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCoursePurchaseResultVO;
 import com.tcm.training.admin.app.course.vo.AppHealthCourseVideoVO;
+import com.tcm.training.admin.app.course.vo.AppMyHealthCourseDetailVO;
+import com.tcm.training.admin.app.course.vo.AppMyHealthCourseItemVO;
+import com.tcm.training.admin.app.course.vo.AppMyHealthCourseVideoItemVO;
 import com.tcm.training.admin.live.entity.CoursePurchaseOrder;
 import com.tcm.training.admin.live.entity.Expert;
 import com.tcm.training.admin.live.entity.HealthCourse;
+import com.tcm.training.admin.live.entity.HealthCourseDownloadRecord;
 import com.tcm.training.admin.live.entity.HealthCourseFavoriteRecord;
 import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord;
+import com.tcm.training.admin.live.entity.HealthCoursePlayRecord;
 import com.tcm.training.admin.live.entity.HealthCourseVideo;
 import com.tcm.training.admin.live.entity.LiveCourseCategory;
 import com.tcm.training.admin.live.mapper.CoursePurchaseOrderMapper;
 import com.tcm.training.admin.live.mapper.ExpertMapper;
+import com.tcm.training.admin.live.mapper.HealthCourseDownloadRecordMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseFavoriteRecordMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper;
+import com.tcm.training.admin.live.mapper.HealthCoursePlayRecordMapper;
 import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper;
 import com.tcm.training.admin.live.mapper.LiveCourseCategoryMapper;
+import com.tcm.training.admin.live.row.AppHealthCourseDownloadRow;
+import com.tcm.training.admin.live.row.AppHealthCourseFavoriteRow;
+import com.tcm.training.admin.live.row.AppMyHealthCourseProgressRow;
+import com.tcm.training.admin.live.row.AppMyHealthCourseVideoProgressRow;
 import com.tcm.training.admin.live.service.HealthCourseAppService;
 import com.tcm.training.admin.live.service.LiveCourseCategoryAppService;
 import com.tcm.training.admin.live.vo.HealthCourseDetailVO;
 import com.tcm.training.admin.live.vo.HealthCourseListRow;
+import com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow;
 import com.tcm.training.admin.member.entity.AppUser;
 import com.tcm.training.admin.member.mapper.UserMapper;
 import com.tcm.training.admin.system.service.SystemSettingsAppService;
@@ -40,8 +59,11 @@
 import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.time.format.DateTimeFormatter;
 
@@ -68,8 +90,10 @@
     private final HealthCourseVideoMapper healthCourseVideoMapper;
     private final LiveCourseCategoryMapper liveCourseCategoryMapper;
     private final ExpertMapper expertMapper;
+    private final HealthCourseDownloadRecordMapper healthCourseDownloadRecordMapper;
     private final HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper;
     private final HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper;
+    private final HealthCoursePlayRecordMapper healthCoursePlayRecordMapper;
     private final AppAuthSessionService appAuthSessionService;
     private final CoursePurchaseOrderMapper coursePurchaseOrderMapper;
     private final UserMapper userMapper;
@@ -83,8 +107,10 @@
                                   HealthCourseVideoMapper healthCourseVideoMapper,
                                   LiveCourseCategoryMapper liveCourseCategoryMapper,
                                   ExpertMapper expertMapper,
+                                  HealthCourseDownloadRecordMapper healthCourseDownloadRecordMapper,
                                   HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper,
                                   HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper,
+                                  HealthCoursePlayRecordMapper healthCoursePlayRecordMapper,
                                   AppAuthSessionService appAuthSessionService,
                                   CoursePurchaseOrderMapper coursePurchaseOrderMapper,
                                   UserMapper userMapper,
@@ -94,8 +120,10 @@
         this.healthCourseVideoMapper = healthCourseVideoMapper;
         this.liveCourseCategoryMapper = liveCourseCategoryMapper;
         this.expertMapper = expertMapper;
+        this.healthCourseDownloadRecordMapper = healthCourseDownloadRecordMapper;
         this.healthCourseFavoriteRecordMapper = healthCourseFavoriteRecordMapper;
         this.healthCourseMemberJoinRecordMapper = healthCourseMemberJoinRecordMapper;
+        this.healthCoursePlayRecordMapper = healthCoursePlayRecordMapper;
         this.appAuthSessionService = appAuthSessionService;
         this.coursePurchaseOrderMapper = coursePurchaseOrderMapper;
         this.userMapper = userMapper;
@@ -168,6 +196,275 @@
         detailVO.setCourseOwned(Boolean.valueOf(purchaseState.isCourseOwned()));
         detailVO.setCoursePurchased(Boolean.valueOf(purchaseState.isCoursePurchased()));
         return detailVO;
+    }
+
+    /**
+     * 获取当前登录用户的收藏课程列表。
+     */
+    public List<AppHealthCourseFavoriteItemVO> favorites() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        List<AppHealthCourseFavoriteRow> rows = healthCourseFavoriteRecordMapper.selectAppFavoriteCourses(userId);
+        if (rows == null || rows.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<AppHealthCourseFavoriteItemVO> result = new ArrayList<AppHealthCourseFavoriteItemVO>(rows.size());
+        for (AppHealthCourseFavoriteRow row : rows) {
+            AppHealthCourseFavoriteItemVO itemVO = new AppHealthCourseFavoriteItemVO();
+            itemVO.setCourseId(row.getCourseId());
+            itemVO.setTitle(trimToEmpty(row.getTitle()));
+            itemVO.setCoverImage(trimToEmpty(row.getCoverImage()));
+            itemVO.setLevelName(trimToEmpty(row.getLevelName()));
+            itemVO.setDifficultyName(trimToEmpty(row.getDifficultyName()));
+            itemVO.setTeacherName(trimToEmpty(row.getTeacherName()));
+            itemVO.setTeacherAvatar(trimToEmpty(row.getTeacherAvatar()));
+            itemVO.setLearnUserCount(defaultNumber(row.getLearnUserCount()));
+            itemVO.setFavoritedAt(row.getFavoritedAt());
+            result.add(itemVO);
+        }
+        return result;
+    }
+
+    /**
+     * 获取当前登录用户的已完成下载列表。
+     */
+    public List<AppHealthCourseDownloadItemVO> downloads() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        List<AppHealthCourseDownloadRow> rows = healthCourseDownloadRecordMapper.selectAppDownloads(userId);
+        if (rows == null || rows.isEmpty()) {
+            return Collections.emptyList();
+        }
+        List<AppHealthCourseDownloadItemVO> result = new ArrayList<AppHealthCourseDownloadItemVO>(rows.size());
+        for (AppHealthCourseDownloadRow row : rows) {
+            AppHealthCourseDownloadItemVO itemVO = new AppHealthCourseDownloadItemVO();
+            itemVO.setId(row.getId());
+            itemVO.setCourseId(row.getCourseId());
+            itemVO.setCourseVideoId(row.getCourseVideoId());
+            itemVO.setTitle(trimToEmpty(row.getTitle()));
+            itemVO.setSubtitle(trimToEmpty(row.getSubtitle()));
+            itemVO.setCoverImage(trimToEmpty(row.getCoverImage()));
+            itemVO.setDurationSeconds(defaultNumber(row.getDurationSeconds()));
+            itemVO.setDurationText(formatVideoDuration(itemVO.getDurationSeconds()));
+            itemVO.setVideoUrl(trimToEmpty(row.getVideoUrl()));
+            itemVO.setDownloadedAt(row.getDownloadedAt());
+            result.add(itemVO);
+        }
+        return result;
+    }
+
+    /**
+     * 获取当前登录用户的我的课程列表。
+     */
+    public List<AppMyHealthCourseItemVO> myCourses() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        List<HealthCourseOwnershipSnapshot> snapshots = listMyCourseSnapshots(userId);
+        if (snapshots.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, CourseProgressSnapshot> progressSnapshotMap = buildCourseProgressSnapshotMap(userId, extractCourseIds(snapshots));
+        List<AppMyHealthCourseItemVO> result = new ArrayList<AppMyHealthCourseItemVO>(snapshots.size());
+        for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
+            AppMyHealthCourseItemVO itemVO = new AppMyHealthCourseItemVO();
+            itemVO.setCourseId(snapshot.getCourseId());
+            itemVO.setTitle(trimToEmpty(snapshot.getTitle()));
+            itemVO.setCoverImage(trimToEmpty(snapshot.getCoverImage()));
+            itemVO.setHasHealthTag(Boolean.TRUE);
+            CourseProgressSnapshot progressSnapshot = progressSnapshotMap.get(snapshot.getCourseId());
+            int completedLessons = progressSnapshot == null ? 0 : progressSnapshot.getCompletedLessons();
+            int totalLessons = snapshot.getTotalLessons();
+            itemVO.setCompletedLessons(Integer.valueOf(completedLessons));
+            itemVO.setTotalLessons(Integer.valueOf(totalLessons));
+            itemVO.setProgressPercent(Integer.valueOf(calculateProgressPercent(completedLessons, totalLessons)));
+            itemVO.setSourceType(snapshot.getSourceType());
+            itemVO.setRemovable(Boolean.valueOf(snapshot.isRemovable()));
+            result.add(itemVO);
+        }
+        return result;
+    }
+
+    /**
+     * 删除当前登录用户的会员加课课程。
+     */
+    @Transactional
+    public void removeMyCourse(Long courseId) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        requireEnabledCourse(courseId);
+        if (findLatestActivePaidCourseOrder(userId, courseId) != null) {
+            throw new IllegalArgumentException("已购买课程不支持移除");
+        }
+        HealthCourseMemberJoinRecord joinRecord = findMemberJoinRecord(courseId, userId);
+        if (joinRecord == null) {
+            throw new IllegalArgumentException("课程不存在或已移除");
+        }
+        healthCourseMemberJoinRecordMapper.update(null, new LambdaUpdateWrapper<HealthCourseMemberJoinRecord>()
+                .eq(HealthCourseMemberJoinRecord::getId, joinRecord.getId())
+                .eq(HealthCourseMemberJoinRecord::getAppUserId, userId)
+                .eq(HealthCourseMemberJoinRecord::getDeleted, 0)
+                .set(HealthCourseMemberJoinRecord::getDeleted, 1));
+    }
+
+    /**
+     * 获取当前登录用户的我的课程详情。
+     */
+    public AppMyHealthCourseDetailVO myCourseDetail(Long courseId) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        HealthCourseOwnershipSnapshot ownershipSnapshot = requireOwnedCourse(userId, courseId);
+        HealthCourse course = requireEnabledCourse(courseId);
+        List<HealthCourseVideo> enabledVideos = listEnabledVideos(courseId);
+        Map<Long, VideoProgressSnapshot> videoProgressSnapshotMap = buildVideoProgressSnapshotMap(userId, Collections.singletonList(courseId));
+
+        List<AppMyHealthCourseVideoItemVO> videos = new ArrayList<AppMyHealthCourseVideoItemVO>(enabledVideos.size());
+        Long currentVideoId = null;
+        LocalDateTime latestLearnAt = null;
+        for (HealthCourseVideo video : enabledVideos) {
+            VideoProgressSnapshot snapshot = videoProgressSnapshotMap.get(video.getId());
+            AppMyHealthCourseVideoItemVO itemVO = new AppMyHealthCourseVideoItemVO();
+            itemVO.setVideoId(video.getId());
+            itemVO.setChapterName(trimToEmpty(video.getChapterName()));
+            itemVO.setDurationSeconds(defaultNumber(video.getDurationSeconds()));
+            itemVO.setDurationText(formatVideoDuration(itemVO.getDurationSeconds()));
+            itemVO.setVideoUrl(trimToEmpty(video.getVideoUrl()));
+            itemVO.setCompleted(Boolean.valueOf(snapshot != null && snapshot.isCompleted()));
+            itemVO.setProgressPercent(Integer.valueOf(calculateVideoDisplayProgressPercent(
+                    snapshot == null ? 0 : snapshot.getCumulativeWatchSeconds(),
+                    itemVO.getDurationSeconds().intValue(),
+                    snapshot != null && snapshot.isCompleted()
+            )));
+            itemVO.setLastPlayPositionSeconds(Integer.valueOf(resolveVideoResumePosition(snapshot)));
+            itemVO.setDownloadable(Boolean.valueOf(hasVideoDownloadAccess(userId, courseId, video.getId())));
+            videos.add(itemVO);
+            if (snapshot != null && snapshot.getLatestLearnAt() != null
+                    && (latestLearnAt == null || snapshot.getLatestLearnAt().isAfter(latestLearnAt))) {
+                latestLearnAt = snapshot.getLatestLearnAt();
+                currentVideoId = video.getId();
+            }
+        }
+        if (currentVideoId == null && !enabledVideos.isEmpty()) {
+            currentVideoId = enabledVideos.get(0).getId();
+        }
+
+        AppMyHealthCourseDetailVO detailVO = new AppMyHealthCourseDetailVO();
+        detailVO.setCourseId(course.getId());
+        detailVO.setTitle(trimToEmpty(course.getTitle()));
+        detailVO.setCourseTitle(trimToEmpty(course.getTitle()));
+        detailVO.setCoverImage(trimToEmpty(course.getCoverImg()));
+        detailVO.setTotalLessons(Integer.valueOf(enabledVideos.size()));
+        detailVO.setTotalDurationText(formatCourseDuration(resolveTotalDurationSecondsByEntity(course, enabledVideos)));
+        detailVO.setCurrentVideoId(currentVideoId);
+        detailVO.setServicePhone(resolveServicePhone());
+        detailVO.setIntroAvailable(Boolean.TRUE);
+        detailVO.setVideos(videos);
+        return detailVO;
+    }
+
+    /**
+     * 上报当前登录用户的我的课程播放记录。
+     */
+    @Transactional
+    public AppHealthCoursePlayRecordUploadResultVO uploadMyCoursePlayRecord(Long courseId,
+                                                                            Long videoId,
+                                                                            AppHealthCoursePlayRecordUploadRequest request) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        requireOwnedCourse(userId, courseId);
+        HealthCourseVideo video = requireEnabledVideo(courseId, videoId);
+        validatePlayRecordRequest(request);
+
+        int videoDurationSeconds = defaultNumber(video.getDurationSeconds());
+        int cumulativeWatchSeconds = resolveExistingCumulativeWatchSeconds(userId, courseId, videoId)
+                + defaultNumber(request.getDurationSeconds());
+        boolean completed = videoDurationSeconds > 0 && cumulativeWatchSeconds >= videoDurationSeconds;
+        int lastPlayPositionSeconds = completed ? 0 : normalizePlayPosition(request.getLastPlayPositionSeconds(), videoDurationSeconds);
+
+        HealthCoursePlayRecord playRecord = new HealthCoursePlayRecord();
+        playRecord.setCourseId(courseId);
+        playRecord.setVideoId(videoId);
+        playRecord.setAppUserId(userId);
+        playRecord.setPlayStart(request.getPlayStart());
+        playRecord.setPlayEnd(request.getPlayEnd());
+        playRecord.setDurationSeconds(defaultNumber(request.getDurationSeconds()));
+        playRecord.setIsCompleted(completed ? 1 : 0);
+        playRecord.setLastPlayPositionSeconds(lastPlayPositionSeconds);
+        healthCoursePlayRecordMapper.insert(playRecord);
+
+        Map<Long, VideoProgressSnapshot> videoProgressSnapshotMap = buildVideoProgressSnapshotMap(userId, Collections.singletonList(courseId));
+        VideoProgressSnapshot videoProgressSnapshot = videoProgressSnapshotMap.get(videoId);
+        CourseProgressSnapshot courseProgressSnapshot = buildCourseProgressSnapshotMap(userId, Collections.singletonList(courseId)).get(courseId);
+
+        AppHealthCoursePlayRecordUploadResultVO resultVO = new AppHealthCoursePlayRecordUploadResultVO();
+        resultVO.setLastPlayPositionSeconds(Integer.valueOf(resolveVideoResumePosition(videoProgressSnapshot)));
+        resultVO.setCompleted(Boolean.valueOf(videoProgressSnapshot != null && videoProgressSnapshot.isCompleted()));
+        resultVO.setProgressPercent(Integer.valueOf(calculateVideoDisplayProgressPercent(
+                videoProgressSnapshot == null ? 0 : videoProgressSnapshot.getCumulativeWatchSeconds(),
+                videoDurationSeconds,
+                videoProgressSnapshot != null && videoProgressSnapshot.isCompleted()
+        )));
+        int totalLessons = listEnabledVideos(courseId).size();
+        int completedLessons = courseProgressSnapshot == null ? 0 : courseProgressSnapshot.getCompletedLessons();
+        resultVO.setCompletedLessons(Integer.valueOf(completedLessons));
+        resultVO.setTotalLessons(Integer.valueOf(totalLessons));
+        resultVO.setCourseProgressPercent(Integer.valueOf(calculateProgressPercent(completedLessons, totalLessons)));
+        return resultVO;
+    }
+
+    /**
+     * 记录当前登录用户的课程小节下载完成状态。
+     */
+    @Transactional
+    public void reportDownload(AppHealthCourseDownloadReportRequest request) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        Long courseId = request == null ? null : request.getCourseId();
+        Long courseVideoId = request == null ? null : request.getCourseVideoId();
+        HealthCourse course = requireEnabledCourse(courseId);
+        HealthCourseVideo video = requireEnabledVideo(courseId, courseVideoId);
+        if (trimToEmpty(video.getVideoUrl()).isEmpty()) {
+            throw new IllegalArgumentException("当前课程小节暂未配置视频地址");
+        }
+        if (!hasVideoDownloadAccess(userId, courseId, courseVideoId)) {
+            throw new IllegalArgumentException("当前课程小节未购买,无法下载");
+        }
+        HealthCourseDownloadRecord existingRecord = findActiveDownloadRecord(userId, courseVideoId);
+        if (existingRecord == null) {
+            HealthCourseDownloadRecord record = buildDownloadRecord(userId, course, video);
+            healthCourseDownloadRecordMapper.insert(record);
+            return;
+        }
+        // 重复下载同一小节时只刷新快照与更新时间,避免列表产生重复项。
+        HealthCourseDownloadRecord refreshRecord = buildDownloadRecord(userId, course, video);
+        refreshRecord.setId(existingRecord.getId());
+        healthCourseDownloadRecordMapper.updateById(refreshRecord);
+    }
+
+    /**
+     * 批量删除当前登录用户的下载记录。
+     */
+    @Transactional
+    public void batchDeleteDownloads(AppHealthCourseDownloadBatchDeleteRequest request) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        Set<Long> recordIds = normalizeDownloadIds(request);
+        if (recordIds.isEmpty()) {
+            throw new IllegalArgumentException("请选择要删除的下载记录");
+        }
+        for (Long recordId : recordIds) {
+            healthCourseDownloadRecordMapper.update(null, new LambdaUpdateWrapper<HealthCourseDownloadRecord>()
+                    .eq(HealthCourseDownloadRecord::getId, recordId)
+                    .eq(HealthCourseDownloadRecord::getAppUserId, userId)
+                    .eq(HealthCourseDownloadRecord::getDeleted, 0)
+                    .set(HealthCourseDownloadRecord::getDeleted, 1));
+        }
+    }
+
+    /**
+     * 批量取消当前登录用户的养生课程收藏。
+     */
+    @Transactional
+    public void batchCancelFavorite(AppHealthCourseFavoriteBatchCancelRequest request) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        Set<Long> courseIds = normalizeCourseIds(request);
+        if (courseIds.isEmpty()) {
+            throw new IllegalArgumentException("请选择要取消收藏的课程");
+        }
+        for (Long courseId : courseIds) {
+            cancelFavoriteIfNeeded(courseId, userId);
+        }
     }
 
     /**
@@ -317,6 +614,242 @@
     }
 
     /**
+     * 汇总我的课程来源,已购课程优先级高于会员加课。
+     */
+    private List<HealthCourseOwnershipSnapshot> listMyCourseSnapshots(Long userId) {
+        Map<Long, HealthCourseOwnershipSnapshot> snapshotMap = new HashMap<Long, HealthCourseOwnershipSnapshot>();
+        List<CoursePurchaseOrderListRow> paidCourseRows = coursePurchaseOrderMapper.selectPaidCourseSnapshots(userId);
+        if (paidCourseRows != null) {
+            for (CoursePurchaseOrderListRow row : paidCourseRows) {
+                Long courseId = row.getCourseId();
+                if (courseId == null || snapshotMap.containsKey(courseId)) {
+                    continue;
+                }
+                HealthCourse course = healthCourseMapper.selectById(courseId);
+                if (!isEnabledCourse(course)) {
+                    continue;
+                }
+                snapshotMap.put(courseId, buildOwnershipSnapshot(course, "PURCHASED", false));
+            }
+        }
+        List<Long> memberCourseIds = healthCourseMemberJoinRecordMapper.selectActiveCourseIdsByUserId(userId);
+        if (memberCourseIds != null) {
+            for (Long courseId : memberCourseIds) {
+                if (courseId == null || snapshotMap.containsKey(courseId)) {
+                    continue;
+                }
+                HealthCourse course = healthCourseMapper.selectById(courseId);
+                if (!isEnabledCourse(course)) {
+                    continue;
+                }
+                snapshotMap.put(courseId, buildOwnershipSnapshot(course, "MEMBER_JOIN", true));
+            }
+        }
+        List<HealthCourseOwnershipSnapshot> snapshots = new ArrayList<HealthCourseOwnershipSnapshot>(snapshotMap.values());
+        Collections.sort(snapshots, Comparator.comparing(HealthCourseOwnershipSnapshot::getCourseId).reversed());
+        return snapshots;
+    }
+
+    /**
+     * 校验课程是否属于当前登录用户的我的课程。
+     */
+    private HealthCourseOwnershipSnapshot requireOwnedCourse(Long userId, Long courseId) {
+        List<HealthCourseOwnershipSnapshot> snapshots = listMyCourseSnapshots(userId);
+        for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
+            if (snapshot.getCourseId().equals(courseId)) {
+                return snapshot;
+            }
+        }
+        throw new IllegalArgumentException("课程不存在或未加入我的课程");
+    }
+
+    /**
+     * 构建课程归属快照,供我的课程相关接口复用。
+     */
+    private HealthCourseOwnershipSnapshot buildOwnershipSnapshot(HealthCourse course, String sourceType, boolean removable) {
+        return new HealthCourseOwnershipSnapshot(
+                course.getId(),
+                trimToEmpty(course.getTitle()),
+                trimToEmpty(course.getCoverImg()),
+                countEnabledVideos(course.getId()),
+                sourceType,
+                removable
+        );
+    }
+
+    /**
+     * 批量构建课程整体进度快照。
+     */
+    private Map<Long, CourseProgressSnapshot> buildCourseProgressSnapshotMap(Long userId, List<Long> courseIds) {
+        if (courseIds == null || courseIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<AppMyHealthCourseProgressRow> rows = healthCoursePlayRecordMapper.selectCourseProgressByCourseIds(userId, courseIds);
+        Map<Long, CourseProgressSnapshot> snapshotMap = new HashMap<Long, CourseProgressSnapshot>();
+        if (rows == null) {
+            return snapshotMap;
+        }
+        for (AppMyHealthCourseProgressRow row : rows) {
+            if (row.getCourseId() == null) {
+                continue;
+            }
+            snapshotMap.put(row.getCourseId(), new CourseProgressSnapshot(
+                    defaultNumber(row.getCompletedLessons()),
+                    row.getLatestLearnAt()
+            ));
+        }
+        return snapshotMap;
+    }
+
+    /**
+     * 批量构建章节进度快照,并以章节ID作为索引键。
+     */
+    private Map<Long, VideoProgressSnapshot> buildVideoProgressSnapshotMap(Long userId, List<Long> courseIds) {
+        if (courseIds == null || courseIds.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        List<AppMyHealthCourseVideoProgressRow> rows = healthCoursePlayRecordMapper.selectVideoProgressByCourseIds(userId, courseIds);
+        Map<Long, VideoProgressSnapshot> snapshotMap = new HashMap<Long, VideoProgressSnapshot>();
+        if (rows == null) {
+            return snapshotMap;
+        }
+        for (AppMyHealthCourseVideoProgressRow row : rows) {
+            if (row.getVideoId() == null) {
+                continue;
+            }
+            snapshotMap.put(row.getVideoId(), new VideoProgressSnapshot(
+                    defaultNumber(row.getCumulativeWatchSeconds()),
+                    row.getCompleted() != null && row.getCompleted().intValue() == 1,
+                    defaultNumber(row.getLastPlayPositionSeconds()),
+                    row.getLatestLearnAt()
+            ));
+        }
+        return snapshotMap;
+    }
+
+    /**
+     * 提取课程主键集合,便于批量查询进度。
+     */
+    private List<Long> extractCourseIds(List<HealthCourseOwnershipSnapshot> snapshots) {
+        List<Long> courseIds = new ArrayList<Long>(snapshots.size());
+        for (HealthCourseOwnershipSnapshot snapshot : snapshots) {
+            courseIds.add(snapshot.getCourseId());
+        }
+        return courseIds;
+    }
+
+    /**
+     * 统计课程启用章节数量。
+     */
+    private int countEnabledVideos(Long courseId) {
+        return listEnabledVideos(courseId).size();
+    }
+
+    /**
+     * 计算百分比进度。
+     */
+    private int calculateProgressPercent(int current, int total) {
+        if (total <= 0 || current <= 0) {
+            return 0;
+        }
+        if (current >= total) {
+            return 100;
+        }
+        return Math.max(Math.min((int) Math.round((current * 100.0D) / total), 100), 0);
+    }
+
+    /**
+     * 计算章节播放进度展示值,未完播章节最高仅展示到99%,避免与完播态混淆。
+     */
+    private int calculateVideoDisplayProgressPercent(int current, int total, boolean completed) {
+        int progressPercent = calculateProgressPercent(current, total);
+        if (!completed && progressPercent >= 100) {
+            return 99;
+        }
+        return progressPercent;
+    }
+
+    /**
+     * 解析章节续播位置,已完播章节再次进入时从头播放。
+     */
+    private int resolveVideoResumePosition(VideoProgressSnapshot snapshot) {
+        if (snapshot == null || snapshot.isCompleted()) {
+            return 0;
+        }
+        return Math.max(snapshot.getLastPlayPositionSeconds(), 0);
+    }
+
+    /**
+     * 校验播放记录上报请求。
+     */
+    private void validatePlayRecordRequest(AppHealthCoursePlayRecordUploadRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("播放记录不能为空");
+        }
+        if (request.getPlayStart() == null || request.getPlayEnd() == null) {
+            throw new IllegalArgumentException("播放时间不能为空");
+        }
+        if (request.getPlayEnd().isBefore(request.getPlayStart())) {
+            throw new IllegalArgumentException("播放结束时间不能早于开始时间");
+        }
+    }
+
+    /**
+     * 查询当前章节已累计观看时长。
+     */
+    private int resolveExistingCumulativeWatchSeconds(Long userId, Long courseId, Long videoId) {
+        List<HealthCoursePlayRecord> records = healthCoursePlayRecordMapper.selectList(new LambdaQueryWrapper<HealthCoursePlayRecord>()
+                .eq(HealthCoursePlayRecord::getAppUserId, userId)
+                .eq(HealthCoursePlayRecord::getCourseId, courseId)
+                .eq(HealthCoursePlayRecord::getVideoId, videoId));
+        if (records == null || records.isEmpty()) {
+            return 0;
+        }
+        int sum = 0;
+        for (HealthCoursePlayRecord record : records) {
+            sum += defaultNumber(record.getDurationSeconds());
+        }
+        return sum;
+    }
+
+    /**
+     * 归一化播放位置,避免超出视频时长。
+     */
+    private int normalizePlayPosition(Integer lastPlayPositionSeconds, int videoDurationSeconds) {
+        int position = defaultNumber(lastPlayPositionSeconds);
+        if (videoDurationSeconds <= 0) {
+            return Math.max(position, 0);
+        }
+        return Math.max(Math.min(position, videoDurationSeconds), 0);
+    }
+
+    /**
+     * 使用实体数据解析课程总时长。
+     */
+    private Integer resolveTotalDurationSecondsByEntity(HealthCourse course, List<HealthCourseVideo> videos) {
+        Integer totalDurationSeconds = course.getTotalDurationSeconds();
+        if (totalDurationSeconds != null && totalDurationSeconds.intValue() > 0) {
+            return totalDurationSeconds;
+        }
+        int sum = 0;
+        for (HealthCourseVideo video : videos) {
+            sum += defaultNumber(video.getDurationSeconds());
+        }
+        return Integer.valueOf(sum);
+    }
+
+    /**
+     * 判断课程是否为启用状态。
+     */
+    private boolean isEnabledCourse(HealthCourse course) {
+        return course != null
+                && course.getDeleted() != null
+                && course.getDeleted().intValue() == 0
+                && course.getStatus() != null
+                && course.getStatus().intValue() == COURSE_STATUS_ENABLED;
+    }
+
+    /**
      * 判断当前请求用户是否已收藏课程;游客统一视为未收藏。
      */
     private boolean resolveFavorited(Long courseId) {
@@ -328,6 +861,54 @@
             return false;
         }
         return isFavorited(findLatestFavoriteRecord(courseId, userId));
+    }
+
+    /**
+     * 按课程集合规范化批量取消入参。
+     */
+    private Set<Long> normalizeCourseIds(AppHealthCourseFavoriteBatchCancelRequest request) {
+        if (request == null || request.getCourseIds() == null || request.getCourseIds().isEmpty()) {
+            return Collections.emptySet();
+        }
+        Set<Long> courseIds = new LinkedHashSet<Long>();
+        for (Long courseId : request.getCourseIds()) {
+            if (courseId != null) {
+                courseIds.add(courseId);
+            }
+        }
+        return courseIds;
+    }
+
+    /**
+     * 按下载记录集合规范化批量删除入参。
+     */
+    private Set<Long> normalizeDownloadIds(AppHealthCourseDownloadBatchDeleteRequest request) {
+        if (request == null || request.getIds() == null || request.getIds().isEmpty()) {
+            return Collections.emptySet();
+        }
+        Set<Long> recordIds = new LinkedHashSet<Long>();
+        for (Long id : request.getIds()) {
+            if (id != null) {
+                recordIds.add(id);
+            }
+        }
+        return recordIds;
+    }
+
+    /**
+     * 在当前课程仍为收藏态时写入取消流水并回收收藏数。
+     */
+    private void cancelFavoriteIfNeeded(Long courseId, Long userId) {
+        HealthCourse course = healthCourseMapper.selectById(courseId);
+        if (course == null || course.getDeleted() == null || course.getDeleted().intValue() != 0) {
+            return;
+        }
+        HealthCourseFavoriteRecord latestRecord = findLatestFavoriteRecord(courseId, userId);
+        if (!isFavorited(latestRecord)) {
+            return;
+        }
+        insertFavoriteRecord(courseId, userId, ACTION_TYPE_CANCEL);
+        updateFavoriteCount(courseId, false);
     }
 
     /**
@@ -495,6 +1076,17 @@
     }
 
     /**
+     * 判断当前用户是否拥有指定小节的下载权限。
+     */
+    private boolean hasVideoDownloadAccess(Long userId, Long courseId, Long courseVideoId) {
+        PurchaseStateSnapshot purchaseState = resolvePurchaseState(courseId);
+        if (purchaseState.isCourseOwned()) {
+            return true;
+        }
+        return purchaseState.getPurchasedVideoIds().contains(courseVideoId);
+    }
+
+    /**
      * 读取当前用户对课程仍有效的已购小节集合。
      */
     private List<Long> resolvePurchasedVideoIds(Long userId, Long courseId, boolean courseOwned) {
@@ -562,6 +1154,36 @@
                 .orderByDesc(CoursePurchaseOrder::getPaidAt)
                 .orderByDesc(CoursePurchaseOrder::getId)
                 .last("LIMIT 1"));
+    }
+
+    /**
+     * 查询当前用户该小节最近一条未删除下载记录。
+     */
+    private HealthCourseDownloadRecord findActiveDownloadRecord(Long userId, Long courseVideoId) {
+        return healthCourseDownloadRecordMapper.selectOne(new LambdaQueryWrapper<HealthCourseDownloadRecord>()
+                .eq(HealthCourseDownloadRecord::getAppUserId, userId)
+                .eq(HealthCourseDownloadRecord::getCourseVideoId, courseVideoId)
+                .eq(HealthCourseDownloadRecord::getDeleted, 0)
+                .orderByDesc(HealthCourseDownloadRecord::getUpdatedAt)
+                .orderByDesc(HealthCourseDownloadRecord::getId)
+                .last("LIMIT 1"));
+    }
+
+    /**
+     * 组装课程小节下载记录快照。
+     */
+    private HealthCourseDownloadRecord buildDownloadRecord(Long userId, HealthCourse course, HealthCourseVideo video) {
+        HealthCourseDownloadRecord record = new HealthCourseDownloadRecord();
+        record.setAppUserId(userId);
+        record.setCourseId(course.getId());
+        record.setCourseVideoId(video.getId());
+        record.setCourseTitleSnapshot(trimToEmpty(course.getTitle()));
+        record.setChapterNameSnapshot(trimToEmpty(video.getChapterName()));
+        record.setCoverImgSnapshot(trimToEmpty(course.getCoverImg()));
+        record.setVideoUrlSnapshot(trimToEmpty(video.getVideoUrl()));
+        record.setDurationSecondsSnapshot(defaultNumber(video.getDurationSeconds()));
+        record.setDeleted(0);
+        return record;
     }
 
     /**
@@ -913,4 +1535,114 @@
             return purchasedVideoIds;
         }
     }
+
+    /**
+     * 我的课程来源快照。
+     */
+    private static final class HealthCourseOwnershipSnapshot {
+
+        private final Long courseId;
+        private final String title;
+        private final String coverImage;
+        private final int totalLessons;
+        private final String sourceType;
+        private final boolean removable;
+
+        private HealthCourseOwnershipSnapshot(Long courseId,
+                                              String title,
+                                              String coverImage,
+                                              int totalLessons,
+                                              String sourceType,
+                                              boolean removable) {
+            this.courseId = courseId;
+            this.title = title;
+            this.coverImage = coverImage;
+            this.totalLessons = totalLessons;
+            this.sourceType = sourceType;
+            this.removable = removable;
+        }
+
+        private Long getCourseId() {
+            return courseId;
+        }
+
+        private String getTitle() {
+            return title;
+        }
+
+        private String getCoverImage() {
+            return coverImage;
+        }
+
+        private int getTotalLessons() {
+            return totalLessons;
+        }
+
+        private String getSourceType() {
+            return sourceType;
+        }
+
+        private boolean isRemovable() {
+            return removable;
+        }
+    }
+
+    /**
+     * 我的课程整体进度快照。
+     */
+    private static final class CourseProgressSnapshot {
+
+        private final int completedLessons;
+        private final LocalDateTime latestLearnAt;
+
+        private CourseProgressSnapshot(int completedLessons, LocalDateTime latestLearnAt) {
+            this.completedLessons = completedLessons;
+            this.latestLearnAt = latestLearnAt;
+        }
+
+        private int getCompletedLessons() {
+            return completedLessons;
+        }
+
+        private LocalDateTime getLatestLearnAt() {
+            return latestLearnAt;
+        }
+    }
+
+    /**
+     * 我的课程章节进度快照。
+     */
+    private static final class VideoProgressSnapshot {
+
+        private final int cumulativeWatchSeconds;
+        private final boolean completed;
+        private final int lastPlayPositionSeconds;
+        private final LocalDateTime latestLearnAt;
+
+        private VideoProgressSnapshot(int cumulativeWatchSeconds,
+                                      boolean completed,
+                                      int lastPlayPositionSeconds,
+                                      LocalDateTime latestLearnAt) {
+            this.cumulativeWatchSeconds = cumulativeWatchSeconds;
+            this.completed = completed;
+            this.lastPlayPositionSeconds = lastPlayPositionSeconds;
+            this.latestLearnAt = latestLearnAt;
+        }
+
+        private int getCumulativeWatchSeconds() {
+            return cumulativeWatchSeconds;
+        }
+
+        private boolean isCompleted() {
+            return completed;
+        }
+
+        private int getLastPlayPositionSeconds() {
+            return lastPlayPositionSeconds;
+        }
+
+        private LocalDateTime getLatestLearnAt() {
+            return latestLearnAt;
+        }
+    }
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderDetailVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderDetailVO.java
new file mode 100644
index 0000000..c284d26
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderDetailVO.java
@@ -0,0 +1,33 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * APP 购课记录详情。
+ */
+@Getter
+@Setter
+public class AppCoursePurchaseOrderDetailVO {
+
+    private Long id;
+    private String orderNo;
+    private Long courseId;
+    private Long courseVideoId;
+    private String productName;
+    private String courseTitle;
+    private String chapterName;
+    private String coverImage;
+    private BigDecimal orderAmount;
+    private String paymentMethod;
+    private String paymentMethodText;
+    private String status;
+    private BigDecimal refundAmount;
+    private Boolean showRefundTag;
+    private LocalDateTime createdAt;
+    private LocalDateTime paidAt;
+    private LocalDateTime refundedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderItemVO.java
new file mode 100644
index 0000000..f8f6204
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppCoursePurchaseOrderItemVO.java
@@ -0,0 +1,30 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * APP 购课记录列表项。
+ */
+@Getter
+@Setter
+public class AppCoursePurchaseOrderItemVO {
+
+    private Long id;
+    private String orderNo;
+    private Long courseId;
+    private Long courseVideoId;
+    private String productName;
+    private String courseTitle;
+    private String chapterName;
+    private String coverImage;
+    private BigDecimal orderAmount;
+    private String paymentMethod;
+    private String paymentMethodText;
+    private String status;
+    private Boolean showRefundTag;
+    private LocalDateTime paidAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseDownloadItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseDownloadItemVO.java
new file mode 100644
index 0000000..cdeb69a
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseDownloadItemVO.java
@@ -0,0 +1,25 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 我的下载列表项。
+ */
+@Getter
+@Setter
+public class AppHealthCourseDownloadItemVO {
+
+    private Long id;
+    private Long courseId;
+    private Long courseVideoId;
+    private String title;
+    private String subtitle;
+    private String coverImage;
+    private Integer durationSeconds;
+    private String durationText;
+    private String videoUrl;
+    private LocalDateTime downloadedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseFavoriteItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseFavoriteItemVO.java
new file mode 100644
index 0000000..81494a3
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCourseFavoriteItemVO.java
@@ -0,0 +1,24 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 收藏课程列表项。
+ */
+@Getter
+@Setter
+public class AppHealthCourseFavoriteItemVO {
+
+    private Long courseId;
+    private String title;
+    private String coverImage;
+    private String levelName;
+    private String difficultyName;
+    private String teacherName;
+    private String teacherAvatar;
+    private Integer learnUserCount;
+    private LocalDateTime favoritedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCoursePlayRecordUploadResultVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCoursePlayRecordUploadResultVO.java
new file mode 100644
index 0000000..2c04d0c
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppHealthCoursePlayRecordUploadResultVO.java
@@ -0,0 +1,19 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * APP 养生课程播放记录上报结果。
+ */
+@Getter
+@Setter
+public class AppHealthCoursePlayRecordUploadResultVO {
+
+    private Integer lastPlayPositionSeconds;
+    private Boolean completed;
+    private Integer progressPercent;
+    private Integer completedLessons;
+    private Integer totalLessons;
+    private Integer courseProgressPercent;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseDetailVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseDetailVO.java
new file mode 100644
index 0000000..f905e44
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseDetailVO.java
@@ -0,0 +1,25 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+
+/**
+ * APP 我的课程详情响应。
+ */
+@Getter
+@Setter
+public class AppMyHealthCourseDetailVO {
+
+    private Long courseId;
+    private String title;
+    private String coverImage;
+    private String courseTitle;
+    private Integer totalLessons;
+    private String totalDurationText;
+    private Long currentVideoId;
+    private String servicePhone;
+    private Boolean introAvailable;
+    private List<AppMyHealthCourseVideoItemVO> videos;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseItemVO.java
new file mode 100644
index 0000000..1c00bbe
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseItemVO.java
@@ -0,0 +1,22 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * APP 我的课程列表项。
+ */
+@Getter
+@Setter
+public class AppMyHealthCourseItemVO {
+
+    private Long courseId;
+    private String title;
+    private String coverImage;
+    private Boolean hasHealthTag;
+    private Integer completedLessons;
+    private Integer totalLessons;
+    private Integer progressPercent;
+    private String sourceType;
+    private Boolean removable;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseVideoItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseVideoItemVO.java
new file mode 100644
index 0000000..6293b58
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/course/vo/AppMyHealthCourseVideoItemVO.java
@@ -0,0 +1,22 @@
+package com.tcm.training.admin.app.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * APP 我的课程详情章节项。
+ */
+@Getter
+@Setter
+public class AppMyHealthCourseVideoItemVO {
+
+    private Long videoId;
+    private String chapterName;
+    private Integer durationSeconds;
+    private String durationText;
+    private String videoUrl;
+    private Boolean completed;
+    private Integer progressPercent;
+    private Integer lastPlayPositionSeconds;
+    private Boolean downloadable;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/controller/AppMemberOpenPageController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/controller/AppMemberOpenPageController.java
index 4238921..128fa00 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/controller/AppMemberOpenPageController.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/controller/AppMemberOpenPageController.java
@@ -3,14 +3,17 @@
 import com.tcm.training.admin.app.member.dto.AppMemberPurchaseRequest;
 import com.tcm.training.admin.app.member.service.AppMemberPurchaseService;
 import com.tcm.training.admin.app.member.service.AppMemberOpenPageService;
+import com.tcm.training.admin.app.member.vo.AppMemberPurchaseOrderItemVO;
 import com.tcm.training.admin.app.member.vo.AppMemberOpenPageVO;
 import com.tcm.training.admin.app.member.vo.AppMemberPurchaseResultVO;
 import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.GetMapping;
 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;
@@ -25,14 +28,17 @@
 
     private final AppMemberOpenPageService appMemberOpenPageService;
     private final AppMemberPurchaseService appMemberPurchaseService;
+    private final com.tcm.training.admin.member.service.MemberPurchaseOrderAppService memberPurchaseOrderAppService;
 
     /**
      * 注入 APP 会员开通页服务。
      */
     public AppMemberOpenPageController(AppMemberOpenPageService appMemberOpenPageService,
-                                       AppMemberPurchaseService appMemberPurchaseService) {
+                                       AppMemberPurchaseService appMemberPurchaseService,
+                                       com.tcm.training.admin.member.service.MemberPurchaseOrderAppService memberPurchaseOrderAppService) {
         this.appMemberOpenPageService = appMemberOpenPageService;
         this.appMemberPurchaseService = appMemberPurchaseService;
+        this.memberPurchaseOrderAppService = memberPurchaseOrderAppService;
     }
 
     /**
@@ -50,4 +56,14 @@
     public ApiResponse<AppMemberPurchaseResultVO> purchase(@Valid @RequestBody AppMemberPurchaseRequest request) {
         return ApiResponse.ok(appMemberPurchaseService.purchase(request));
     }
+
+    /**
+     * 获取当前登录用户的会员购买记录。
+     */
+    @GetMapping("/purchase-orders")
+    public ApiResponse<PageResponse<AppMemberPurchaseOrderItemVO>> purchaseOrders(
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize) {
+        return ApiResponse.ok(memberPurchaseOrderAppService.appUserPage(page, pageSize));
+    }
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/vo/AppMemberPurchaseOrderItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/vo/AppMemberPurchaseOrderItemVO.java
new file mode 100644
index 0000000..a6beb0b
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/member/vo/AppMemberPurchaseOrderItemVO.java
@@ -0,0 +1,27 @@
+package com.tcm.training.admin.app.member.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * APP 会员购买记录列表项。
+ */
+@Getter
+@Setter
+public class AppMemberPurchaseOrderItemVO {
+
+    private Long id;
+    private String orderNo;
+    private String memberType;
+    private String memberTypeText;
+    private BigDecimal orderAmount;
+    private String paymentMethod;
+    private String paymentMethodText;
+    private String status;
+    private BigDecimal refundAmount;
+    private Boolean showRefundTag;
+    private LocalDateTime paidAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsLedgerController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsLedgerController.java
new file mode 100644
index 0000000..ae8812e
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsLedgerController.java
@@ -0,0 +1,40 @@
+package com.tcm.training.admin.app.points.controller;
+
+import com.tcm.training.admin.app.points.service.AppPointsLedgerService;
+import com.tcm.training.admin.app.points.vo.AppPointsLedgerItemVO;
+import com.tcm.training.admin.common.ApiResponse;
+import com.tcm.training.admin.common.PageResponse;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * APP 积分明细接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/app/points", "/api/v1/app/points"})
+public class AppPointsLedgerController {
+
+    private final AppPointsLedgerService appPointsLedgerService;
+
+    /**
+     * 注入 APP 积分明细服务。
+     */
+    public AppPointsLedgerController(AppPointsLedgerService appPointsLedgerService) {
+        this.appPointsLedgerService = appPointsLedgerService;
+    }
+
+    /**
+     * 分页查询当前登录用户的积分明细。
+     */
+    @GetMapping("/ledger")
+    public ApiResponse<PageResponse<AppPointsLedgerItemVO>> page(
+            @RequestParam(required = false, defaultValue = "1") int page,
+            @RequestParam(required = false, defaultValue = "20") int pageSize,
+            @RequestParam(required = false) String incomeExpenseType) {
+        return ApiResponse.ok(appPointsLedgerService.page(page, pageSize, incomeExpenseType));
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsOverviewController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsOverviewController.java
new file mode 100644
index 0000000..525618c
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/controller/AppPointsOverviewController.java
@@ -0,0 +1,35 @@
+package com.tcm.training.admin.app.points.controller;
+
+import com.tcm.training.admin.app.points.service.AppPointsOverviewService;
+import com.tcm.training.admin.app.points.vo.AppPointsOverviewVO;
+import com.tcm.training.admin.common.ApiResponse;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * APP 我的积分页面接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/app/points", "/api/v1/app/points"})
+public class AppPointsOverviewController {
+
+    private final AppPointsOverviewService appPointsOverviewService;
+
+    /**
+     * 注入 APP 我的积分页面服务。
+     */
+    public AppPointsOverviewController(AppPointsOverviewService appPointsOverviewService) {
+        this.appPointsOverviewService = appPointsOverviewService;
+    }
+
+    /**
+     * 获取当前登录用户的积分页面聚合数据。
+     */
+    @GetMapping("/overview")
+    public ApiResponse<AppPointsOverviewVO> overview() {
+        return ApiResponse.ok(appPointsOverviewService.getCurrentOverview());
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsLedgerService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsLedgerService.java
new file mode 100644
index 0000000..475f636
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsLedgerService.java
@@ -0,0 +1,113 @@
+package com.tcm.training.admin.app.points.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.points.vo.AppPointsLedgerItemVO;
+import com.tcm.training.admin.common.PageResponse;
+import com.tcm.training.admin.points.row.PointsLedgerRow;
+import com.tcm.training.admin.points.service.PointsLedgerAppService;
+import com.tcm.training.admin.points.mapper.PointsLedgerMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * APP 积分明细服务。
+ */
+@Service
+public class AppPointsLedgerService {
+
+    private final AppAuthSessionService appAuthSessionService;
+    private final PointsLedgerMapper pointsLedgerMapper;
+    private final PointsLedgerAppService pointsLedgerAppService;
+
+    /**
+     * 注入 APP 积分明细所需依赖。
+     */
+    public AppPointsLedgerService(AppAuthSessionService appAuthSessionService,
+                                  PointsLedgerMapper pointsLedgerMapper,
+                                  PointsLedgerAppService pointsLedgerAppService) {
+        this.appAuthSessionService = appAuthSessionService;
+        this.pointsLedgerMapper = pointsLedgerMapper;
+        this.pointsLedgerAppService = pointsLedgerAppService;
+    }
+
+    /**
+     * 分页查询当前登录用户的积分明细。
+     */
+    public PageResponse<AppPointsLedgerItemVO> page(int page, int pageSize, String incomeExpenseType) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        int normalizedPage = page <= 0 ? 1 : page;
+        int normalizedPageSize = pageSize <= 0 ? 20 : pageSize;
+        pointsLedgerAppService.syncLedger();
+        IPage<PointsLedgerRow> result = pointsLedgerMapper.selectAppLedgerPage(
+                new Page<PointsLedgerRow>(normalizedPage, normalizedPageSize),
+                userId,
+                trimToNull(incomeExpenseType)
+        );
+        return PageResponse.of(
+                toItems(result.getRecords()),
+                result.getTotal(),
+                result.getCurrent(),
+                result.getSize()
+        );
+    }
+
+    /**
+     * 将积分流水行转换为 APP 页面需要的列表结构。
+     */
+    private List<AppPointsLedgerItemVO> toItems(List<PointsLedgerRow> rows) {
+        List<AppPointsLedgerItemVO> list = new ArrayList<AppPointsLedgerItemVO>();
+        if (rows == null) {
+            return list;
+        }
+        for (PointsLedgerRow row : rows) {
+            AppPointsLedgerItemVO item = new AppPointsLedgerItemVO();
+            item.setId(row.getId());
+            item.setTitle(buildTitle(row));
+            item.setIncomeExpenseType(trimToEmpty(row.getIncomeExpenseType()));
+            item.setPointsDelta(row.getPointsDelta() == null ? 0 : row.getPointsDelta());
+            item.setOperatedAt(row.getOperatedAt());
+            list.add(item);
+        }
+        return list;
+    }
+
+    /**
+     * 构建 APP 积分明细标题,保持与静态稿一致的业务文案。
+     */
+    private String buildTitle(PointsLedgerRow row) {
+        String contentName = trimToEmpty(row == null ? null : row.getContentName());
+        if (contentName.isEmpty()) {
+            contentName = "-";
+        }
+        String businessType = trimToEmpty(row == null ? null : row.getBusinessType());
+        if (PointsLedgerAppService.BUSINESS_LIVE_WATCH.equals(businessType)) {
+            return "观看课程【" + contentName + "】";
+        }
+        if (PointsLedgerAppService.BUSINESS_POINTS_EXCHANGE.equals(businessType)) {
+            return "兑换礼品【" + contentName + "】";
+        }
+        return "积分变动【" + contentName + "】";
+    }
+
+    /**
+     * 将可空文本归一化为空字符串。
+     */
+    private String trimToEmpty(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.trim();
+    }
+
+    /**
+     * 将空白筛选值归一化为 null,便于 MyBatis 条件判断。
+     */
+    private String trimToNull(String value) {
+        String text = trimToEmpty(value);
+        return text.isEmpty() ? null : text;
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsOverviewService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsOverviewService.java
new file mode 100644
index 0000000..53c0b5c
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/service/AppPointsOverviewService.java
@@ -0,0 +1,138 @@
+package com.tcm.training.admin.app.points.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.points.vo.AppPointsOverviewVO;
+import com.tcm.training.admin.live.entity.Clinic;
+import com.tcm.training.admin.live.mapper.ClinicMapper;
+import com.tcm.training.admin.member.entity.AppUser;
+import com.tcm.training.admin.member.mapper.UserMapper;
+import com.tcm.training.admin.points.entity.PointsConfig;
+import com.tcm.training.admin.points.entity.UserPointsAccount;
+import com.tcm.training.admin.points.mapper.UserPointsAccountMapper;
+import com.tcm.training.admin.points.service.PointsSettingsAppService;
+import com.tcm.training.admin.points.vo.PointsConfigVO;
+import org.springframework.stereotype.Service;
+
+/**
+ * APP 我的积分页面聚合服务。
+ */
+@Service
+public class AppPointsOverviewService {
+
+    private static final int USER_STATUS_ENABLED = 1;
+
+    private final AppAuthSessionService appAuthSessionService;
+    private final UserMapper userMapper;
+    private final UserPointsAccountMapper userPointsAccountMapper;
+    private final ClinicMapper clinicMapper;
+    private final PointsSettingsAppService pointsSettingsAppService;
+
+    /**
+     * 注入 APP 我的积分页面所需依赖。
+     */
+    public AppPointsOverviewService(AppAuthSessionService appAuthSessionService,
+                                    UserMapper userMapper,
+                                    UserPointsAccountMapper userPointsAccountMapper,
+                                    ClinicMapper clinicMapper,
+                                    PointsSettingsAppService pointsSettingsAppService) {
+        this.appAuthSessionService = appAuthSessionService;
+        this.userMapper = userMapper;
+        this.userPointsAccountMapper = userPointsAccountMapper;
+        this.clinicMapper = clinicMapper;
+        this.pointsSettingsAppService = pointsSettingsAppService;
+    }
+
+    /**
+     * 获取当前登录用户的积分页面聚合数据。
+     */
+    public AppPointsOverviewVO getCurrentOverview() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        AppUser user = requireEnabledUser(userId);
+        PointsConfigVO config = pointsSettingsAppService.current();
+
+        int pointsBalance = resolvePointsBalance(userId);
+        int requiredPoints = config == null || config.getRequiredPoints() == null ? 0 : Math.max(config.getRequiredPoints().intValue(), 0);
+
+        AppPointsOverviewVO result = new AppPointsOverviewVO();
+        result.setPointsBalance(Integer.valueOf(pointsBalance));
+        result.setRequiredPoints(Integer.valueOf(requiredPoints));
+        result.setProgressPercent(Integer.valueOf(calculateProgressPercent(pointsBalance, requiredPoints)));
+        result.setPointsShortfall(Integer.valueOf(Math.max(requiredPoints - pointsBalance, 0)));
+        result.setActivityStartAt(config == null ? null : config.getActivityStartAt());
+        result.setActivityEndAt(config == null ? null : config.getActivityEndAt());
+        result.setGiftCover(trimToEmpty(config == null ? null : config.getGiftCover()));
+        result.setGiftName(trimToEmpty(config == null ? null : config.getGiftName()));
+
+        Clinic clinic = resolveBoundClinic(user);
+        result.setClinicName(trimToEmpty(clinic == null ? null : clinic.getName()));
+        result.setClinicAddress(trimToEmpty(clinic == null ? null : clinic.getAddress()));
+        return result;
+    }
+
+    /**
+     * 校验当前登录用户存在且可用。
+     */
+    private AppUser requireEnabledUser(Long userId) {
+        AppUser user = userMapper.selectById(userId);
+        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
+            throw new IllegalArgumentException("用户不存在");
+        }
+        if (user.getStatus() == null || user.getStatus().intValue() != USER_STATUS_ENABLED) {
+            throw new IllegalArgumentException("用户已禁用");
+        }
+        return user;
+    }
+
+    /**
+     * 读取当前用户积分余额,没有积分账户时按0返回。
+     */
+    private int resolvePointsBalance(Long userId) {
+        UserPointsAccount account = userPointsAccountMapper.selectOne(new LambdaQueryWrapper<UserPointsAccount>()
+                .eq(UserPointsAccount::getAppUserId, userId)
+                .eq(UserPointsAccount::getDeleted, 0)
+                .orderByDesc(UserPointsAccount::getId)
+                .last("LIMIT 1"));
+        if (account == null || account.getPointsBalance() == null) {
+            return 0;
+        }
+        return Math.max(account.getPointsBalance().intValue(), 0);
+    }
+
+    /**
+     * 读取当前用户绑定诊所,不存在时返回空。
+     */
+    private Clinic resolveBoundClinic(AppUser user) {
+        if (user == null || user.getClinicId() == null) {
+            return null;
+        }
+        Clinic clinic = clinicMapper.selectById(user.getClinicId());
+        if (clinic == null || clinic.getDeleted() == null || clinic.getDeleted().intValue() != 0) {
+            return null;
+        }
+        return clinic;
+    }
+
+    /**
+     * 计算积分进度百分比,避免返回非法值。
+     */
+    private int calculateProgressPercent(int current, int total) {
+        if (total <= 0 || current <= 0) {
+            return 0;
+        }
+        if (current >= total) {
+            return 100;
+        }
+        return Math.max(Math.min((int) Math.round((current * 100.0D) / total), 100), 0);
+    }
+
+    /**
+     * 将可空文本归一化为空字符串。
+     */
+    private String trimToEmpty(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.trim();
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsLedgerItemVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsLedgerItemVO.java
new file mode 100644
index 0000000..bc885d6
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsLedgerItemVO.java
@@ -0,0 +1,20 @@
+package com.tcm.training.admin.app.points.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 积分明细列表项。
+ */
+@Getter
+@Setter
+public class AppPointsLedgerItemVO {
+
+    private Long id;
+    private String title;
+    private String incomeExpenseType;
+    private Integer pointsDelta;
+    private LocalDateTime operatedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsOverviewVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsOverviewVO.java
new file mode 100644
index 0000000..bce8f27
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/points/vo/AppPointsOverviewVO.java
@@ -0,0 +1,25 @@
+package com.tcm.training.admin.app.points.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 我的积分页面聚合数据响应。
+ */
+@Getter
+@Setter
+public class AppPointsOverviewVO {
+
+    private Integer pointsBalance;
+    private Integer requiredPoints;
+    private Integer progressPercent;
+    private Integer pointsShortfall;
+    private LocalDateTime activityStartAt;
+    private LocalDateTime activityEndAt;
+    private String giftCover;
+    private String giftName;
+    private String clinicName;
+    private String clinicAddress;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserCenterController.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserCenterController.java
new file mode 100644
index 0000000..aed98c5
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/controller/AppUserCenterController.java
@@ -0,0 +1,35 @@
+package com.tcm.training.admin.app.user.controller;
+
+import com.tcm.training.admin.app.user.service.AppUserCenterService;
+import com.tcm.training.admin.app.user.vo.AppUserCenterVO;
+import com.tcm.training.admin.common.ApiResponse;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * APP 个人中心接口。
+ */
+@Validated
+@RestController
+@RequestMapping({"/api/app", "/api/v1/app"})
+public class AppUserCenterController {
+
+    private final AppUserCenterService appUserCenterService;
+
+    /**
+     * 注入 APP 个人中心服务。
+     */
+    public AppUserCenterController(AppUserCenterService appUserCenterService) {
+        this.appUserCenterService = appUserCenterService;
+    }
+
+    /**
+     * 获取当前登录用户的个人中心聚合信息。
+     */
+    @GetMapping("/user-center")
+    public ApiResponse<AppUserCenterVO> getCurrentUserCenter() {
+        return ApiResponse.ok(appUserCenterService.getCurrentUserCenter());
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/service/AppUserCenterService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/service/AppUserCenterService.java
new file mode 100644
index 0000000..56ec3b4
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/service/AppUserCenterService.java
@@ -0,0 +1,154 @@
+package com.tcm.training.admin.app.user.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.tcm.training.admin.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.user.vo.AppUserCenterVO;
+import com.tcm.training.admin.live.entity.Clinic;
+import com.tcm.training.admin.live.mapper.ClinicMapper;
+import com.tcm.training.admin.member.entity.AppUser;
+import com.tcm.training.admin.member.mapper.UserMapper;
+import com.tcm.training.admin.points.entity.UserPointsAccount;
+import com.tcm.training.admin.points.mapper.UserPointsAccountMapper;
+import org.springframework.stereotype.Service;
+
+/**
+ * APP 个人中心聚合服务。
+ */
+@Service
+public class AppUserCenterService {
+
+    private static final int USER_STATUS_ENABLED = 1;
+    private static final int USER_TYPE_PERSONAL = 0;
+    private static final int USER_TYPE_DOCTOR = 1;
+
+    private final AppAuthSessionService appAuthSessionService;
+    private final UserMapper userMapper;
+    private final UserPointsAccountMapper userPointsAccountMapper;
+    private final ClinicMapper clinicMapper;
+
+    /**
+     * 注入 APP 个人中心聚合所需依赖。
+     */
+    public AppUserCenterService(AppAuthSessionService appAuthSessionService,
+                                UserMapper userMapper,
+                                UserPointsAccountMapper userPointsAccountMapper,
+                                ClinicMapper clinicMapper) {
+        this.appAuthSessionService = appAuthSessionService;
+        this.userMapper = userMapper;
+        this.userPointsAccountMapper = userPointsAccountMapper;
+        this.clinicMapper = clinicMapper;
+    }
+
+    /**
+     * 获取当前登录用户的个人中心聚合信息。
+     */
+    public AppUserCenterVO getCurrentUserCenter() {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        AppUser user = requireEnabledUser(userId);
+        AppUserCenterVO result = new AppUserCenterVO();
+        result.setId(user.getId());
+        result.setNickname(resolveNickname(user));
+        result.setAvatar(resolveAvatar(user));
+        result.setPhone(trimToEmpty(user.getPhone()));
+        result.setType(user.getType());
+        result.setTypeText(resolveTypeText(user.getType()));
+        result.setMemberExpireAt(user.getMemberExpireAt());
+        result.setPointsBalance(resolvePointsBalance(user.getId()));
+        result.setClinicId(user.getClinicId());
+        // 医生用户才需要回显所属诊所,个人用户统一返回空诊所名称。
+        result.setClinicName(resolveClinicName(user));
+        return result;
+    }
+
+    /**
+     * 校验当前登录用户存在且可用。
+     */
+    private AppUser requireEnabledUser(Long userId) {
+        AppUser user = userMapper.selectById(userId);
+        if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) {
+            throw new IllegalArgumentException("用户不存在");
+        }
+        if (user.getStatus() == null || user.getStatus().intValue() != USER_STATUS_ENABLED) {
+            throw new IllegalArgumentException("用户已禁用");
+        }
+        return user;
+    }
+
+    /**
+     * 读取当前用户积分余额,没有积分账户时按 0 返回。
+     */
+    private Integer resolvePointsBalance(Long userId) {
+        UserPointsAccount account = userPointsAccountMapper.selectOne(new LambdaQueryWrapper<UserPointsAccount>()
+                .eq(UserPointsAccount::getAppUserId, userId)
+                .eq(UserPointsAccount::getDeleted, 0)
+                .orderByDesc(UserPointsAccount::getId)
+                .last("LIMIT 1"));
+        if (account == null || account.getPointsBalance() == null) {
+            return Integer.valueOf(0);
+        }
+        return account.getPointsBalance();
+    }
+
+    /**
+     * 读取医生用户所属诊所名称,不满足条件时返回空字符串。
+     */
+    private String resolveClinicName(AppUser user) {
+        if (user == null || user.getType() == null || user.getType().intValue() != USER_TYPE_DOCTOR || user.getClinicId() == null) {
+            return "";
+        }
+        Clinic clinic = clinicMapper.selectById(user.getClinicId());
+        if (clinic == null || clinic.getDeleted() == null || clinic.getDeleted().intValue() != 0) {
+            return "";
+        }
+        return trimToEmpty(clinic.getName());
+    }
+
+    /**
+     * 解析用户类型展示文案。
+     */
+    private String resolveTypeText(Integer userType) {
+        if (userType != null && userType.intValue() == USER_TYPE_DOCTOR) {
+            return "医生";
+        }
+        if (userType != null && userType.intValue() == USER_TYPE_PERSONAL) {
+            return "个人";
+        }
+        return "";
+    }
+
+    /**
+     * 优先使用本地昵称,没有时回退微信昵称和手机号。
+     */
+    private String resolveNickname(AppUser user) {
+        String nickname = trimToEmpty(user == null ? null : user.getNickname());
+        if (!nickname.isEmpty()) {
+            return nickname;
+        }
+        String wechatNickname = trimToEmpty(user == null ? null : user.getWechatNickname());
+        if (!wechatNickname.isEmpty()) {
+            return wechatNickname;
+        }
+        return trimToEmpty(user == null ? null : user.getPhone());
+    }
+
+    /**
+     * 优先使用本地头像,没有时回退微信头像。
+     */
+    private String resolveAvatar(AppUser user) {
+        String avatar = trimToEmpty(user == null ? null : user.getAvatar());
+        if (!avatar.isEmpty()) {
+            return avatar;
+        }
+        return trimToEmpty(user == null ? null : user.getWechatAvatar());
+    }
+
+    /**
+     * 将可空文本归一化为空字符串。
+     */
+    private String trimToEmpty(String value) {
+        if (value == null) {
+            return "";
+        }
+        return value.trim();
+    }
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/vo/AppUserCenterVO.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/vo/AppUserCenterVO.java
new file mode 100644
index 0000000..b47c63d
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/user/vo/AppUserCenterVO.java
@@ -0,0 +1,25 @@
+package com.tcm.training.admin.app.user.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 个人中心聚合数据响应。
+ */
+@Getter
+@Setter
+public class AppUserCenterVO {
+
+    private Long id;
+    private String nickname;
+    private String avatar;
+    private String phone;
+    private Integer type;
+    private String typeText;
+    private LocalDateTime memberExpireAt;
+    private Integer pointsBalance;
+    private Long clinicId;
+    private String clinicName;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCourseDownloadRecord.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCourseDownloadRecord.java
new file mode 100644
index 0000000..a40fcd6
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCourseDownloadRecord.java
@@ -0,0 +1,65 @@
+package com.tcm.training.admin.live.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_health_course_download_record")
+@Getter
+@Setter
+@NoArgsConstructor
+public class HealthCourseDownloadRecord {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("app_user_id")
+    private Long appUserId;
+
+    @TableField("course_id")
+    private Long courseId;
+
+    @TableField("course_video_id")
+    private Long courseVideoId;
+
+    @TableField("course_title_snapshot")
+    private String courseTitleSnapshot;
+
+    @TableField("chapter_name_snapshot")
+    private String chapterNameSnapshot;
+
+    @TableField("cover_img_snapshot")
+    private String coverImgSnapshot;
+
+    @TableField("video_url_snapshot")
+    private String videoUrlSnapshot;
+
+    @TableField("duration_seconds_snapshot")
+    private Integer durationSecondsSnapshot;
+
+    @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/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCoursePlayRecord.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCoursePlayRecord.java
new file mode 100644
index 0000000..001ebda
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/entity/HealthCoursePlayRecord.java
@@ -0,0 +1,48 @@
+package com.tcm.training.admin.live.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * 养生课程视频播放记录。
+ */
+@TableName("tcm_health_course_play_record")
+@Getter
+@Setter
+@NoArgsConstructor
+public class HealthCoursePlayRecord {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    @TableField("course_id")
+    private Long courseId;
+
+    @TableField("video_id")
+    private Long videoId;
+
+    @TableField("app_user_id")
+    private Long appUserId;
+
+    @TableField("play_start")
+    private LocalDateTime playStart;
+
+    @TableField("play_end")
+    private LocalDateTime playEnd;
+
+    @TableField("duration_seconds")
+    private Integer durationSeconds;
+
+    @TableField("is_completed")
+    private Integer isCompleted;
+
+    @TableField("last_play_position_seconds")
+    private Integer lastPlayPositionSeconds;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/CoursePurchaseOrderMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/CoursePurchaseOrderMapper.java
index f954c92..45ffc00 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/CoursePurchaseOrderMapper.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/CoursePurchaseOrderMapper.java
@@ -22,6 +22,13 @@
                                                       @Param("createdFrom") LocalDateTime createdFrom,
                                                       @Param("createdTo") LocalDateTime createdTo);
 
+    IPage<CoursePurchaseOrderListRow> selectAppUserOrderPage(IPage<CoursePurchaseOrderListRow> page,
+                                                             @Param("appUserId") Long appUserId,
+                                                             @Param("productName") String productName);
+
+    CoursePurchaseOrderListRow selectAppUserOrderDetail(@Param("appUserId") Long appUserId,
+                                                        @Param("orderNo") String orderNo);
+
     List<CoursePurchaseOrderListRow> selectOrderList(@Param("orderNo") String orderNo,
                                                      @Param("userPhone") String userPhone,
                                                      @Param("productName") String productName,
@@ -29,4 +36,6 @@
                                                      @Param("status") String status,
                                                      @Param("createdFrom") LocalDateTime createdFrom,
                                                      @Param("createdTo") LocalDateTime createdTo);
+
+    List<CoursePurchaseOrderListRow> selectPaidCourseSnapshots(@Param("appUserId") Long appUserId);
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseDownloadRecordMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseDownloadRecordMapper.java
new file mode 100644
index 0000000..ee170ec
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseDownloadRecordMapper.java
@@ -0,0 +1,21 @@
+package com.tcm.training.admin.live.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.live.entity.HealthCourseDownloadRecord;
+import com.tcm.training.admin.live.row.AppHealthCourseDownloadRow;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 养生课程下载记录 Mapper。
+ */
+@Mapper
+public interface HealthCourseDownloadRecordMapper extends BaseMapper<HealthCourseDownloadRecord> {
+
+    /**
+     * 查询当前用户的已完成下载记录列表。
+     */
+    List<AppHealthCourseDownloadRow> selectAppDownloads(@Param("appUserId") Long appUserId);
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseFavoriteRecordMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseFavoriteRecordMapper.java
index 78ac6fd..8225bba 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseFavoriteRecordMapper.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseFavoriteRecordMapper.java
@@ -2,11 +2,17 @@
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.tcm.training.admin.live.entity.HealthCourseFavoriteRecord;
+import com.tcm.training.admin.live.row.AppHealthCourseFavoriteRow;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * 养生课程收藏流水 Mapper。
  */
 @Mapper
 public interface HealthCourseFavoriteRecordMapper extends BaseMapper<HealthCourseFavoriteRecord> {
+
+    List<AppHealthCourseFavoriteRow> selectAppFavoriteCourses(@Param("appUserId") Long appUserId);
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseMemberJoinRecordMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseMemberJoinRecordMapper.java
index 3cbc42a..757148c 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseMemberJoinRecordMapper.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCourseMemberJoinRecordMapper.java
@@ -3,10 +3,18 @@
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
 
 /**
  * 养生课程会员加课记录 Mapper。
  */
 @Mapper
 public interface HealthCourseMemberJoinRecordMapper extends BaseMapper<HealthCourseMemberJoinRecord> {
+
+    /**
+     * 查询用户当前有效的会员加课课程主键集合。
+     */
+    List<Long> selectActiveCourseIdsByUserId(@Param("appUserId") Long appUserId);
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCoursePlayRecordMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCoursePlayRecordMapper.java
new file mode 100644
index 0000000..f711030
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/mapper/HealthCoursePlayRecordMapper.java
@@ -0,0 +1,29 @@
+package com.tcm.training.admin.live.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.tcm.training.admin.live.entity.HealthCoursePlayRecord;
+import com.tcm.training.admin.live.row.AppMyHealthCourseProgressRow;
+import com.tcm.training.admin.live.row.AppMyHealthCourseVideoProgressRow;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 养生课程视频播放记录 Mapper。
+ */
+@Mapper
+public interface HealthCoursePlayRecordMapper extends BaseMapper<HealthCoursePlayRecord> {
+
+    /**
+     * 查询指定用户在课程集合下的章节进度汇总。
+     */
+    List<AppMyHealthCourseVideoProgressRow> selectVideoProgressByCourseIds(@Param("appUserId") Long appUserId,
+                                                                           @Param("courseIds") List<Long> courseIds);
+
+    /**
+     * 查询指定用户在课程集合下的课程整体进度汇总。
+     */
+    List<AppMyHealthCourseProgressRow> selectCourseProgressByCourseIds(@Param("appUserId") Long appUserId,
+                                                                       @Param("courseIds") List<Long> courseIds);
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseDownloadRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseDownloadRow.java
new file mode 100644
index 0000000..e003e54
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseDownloadRow.java
@@ -0,0 +1,24 @@
+package com.tcm.training.admin.live.row;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 养生课程下载列表行对象。
+ */
+@Getter
+@Setter
+public class AppHealthCourseDownloadRow {
+
+    private Long id;
+    private Long courseId;
+    private Long courseVideoId;
+    private String title;
+    private String subtitle;
+    private String coverImage;
+    private Integer durationSeconds;
+    private String videoUrl;
+    private LocalDateTime downloadedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseFavoriteRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseFavoriteRow.java
new file mode 100644
index 0000000..d044f82
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppHealthCourseFavoriteRow.java
@@ -0,0 +1,26 @@
+package com.tcm.training.admin.live.row;
+
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 收藏课程查询行。
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class AppHealthCourseFavoriteRow {
+
+    private Long courseId;
+    private String title;
+    private String coverImage;
+    private String levelName;
+    private String difficultyName;
+    private String teacherName;
+    private String teacherAvatar;
+    private Integer learnUserCount;
+    private LocalDateTime favoritedAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseProgressRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseProgressRow.java
new file mode 100644
index 0000000..fa39ad6
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseProgressRow.java
@@ -0,0 +1,18 @@
+package com.tcm.training.admin.live.row;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 我的课程整体进度聚合行。
+ */
+@Getter
+@Setter
+public class AppMyHealthCourseProgressRow {
+
+    private Long courseId;
+    private Integer completedLessons;
+    private LocalDateTime latestLearnAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseVideoProgressRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseVideoProgressRow.java
new file mode 100644
index 0000000..12c729d
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/row/AppMyHealthCourseVideoProgressRow.java
@@ -0,0 +1,21 @@
+package com.tcm.training.admin.live.row;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.LocalDateTime;
+
+/**
+ * APP 我的课程章节进度聚合行。
+ */
+@Getter
+@Setter
+public class AppMyHealthCourseVideoProgressRow {
+
+    private Long courseId;
+    private Long videoId;
+    private Integer cumulativeWatchSeconds;
+    private Integer completed;
+    private Integer lastPlayPositionSeconds;
+    private LocalDateTime latestLearnAt;
+}
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/CoursePurchaseOrderAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/CoursePurchaseOrderAppService.java
index c3daeb1..31d9d23 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/CoursePurchaseOrderAppService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/service/CoursePurchaseOrderAppService.java
@@ -4,6 +4,9 @@
 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.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.course.vo.AppCoursePurchaseOrderDetailVO;
+import com.tcm.training.admin.app.course.vo.AppCoursePurchaseOrderItemVO;
 import com.tcm.training.admin.common.DateTimes;
 import com.tcm.training.admin.common.PageResponse;
 import com.tcm.training.admin.common.PhoneMasker;
@@ -36,11 +39,20 @@
     private static final String PAYMENT_METHOD_ALIPAY = "ALIPAY";
 
     private final CoursePurchaseOrderMapper coursePurchaseOrderMapper;
+    private final AppAuthSessionService appAuthSessionService;
 
-    public CoursePurchaseOrderAppService(CoursePurchaseOrderMapper coursePurchaseOrderMapper) {
+    /**
+     * 注入养生课程购买订单服务依赖。
+     */
+    public CoursePurchaseOrderAppService(CoursePurchaseOrderMapper coursePurchaseOrderMapper,
+                                         AppAuthSessionService appAuthSessionService) {
         this.coursePurchaseOrderMapper = coursePurchaseOrderMapper;
+        this.appAuthSessionService = appAuthSessionService;
     }
 
+    /**
+     * 分页查询后台养生课程购买订单。
+     */
     public PageResponse<CoursePurchaseOrderListItemVO> page(int page,
                                                             int pageSize,
                                                             String orderNo,
@@ -63,6 +75,34 @@
         return PageResponse.of(toList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize());
     }
 
+    /**
+     * 分页查询当前登录用户的 APP 购课记录。
+     */
+    public PageResponse<AppCoursePurchaseOrderItemVO> appUserPage(int page, int pageSize, String productName) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        IPage<CoursePurchaseOrderListRow> result = coursePurchaseOrderMapper.selectAppUserOrderPage(
+                new Page<CoursePurchaseOrderListRow>(page, pageSize),
+                userId,
+                trimToNull(productName)
+        );
+        return PageResponse.of(toAppList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    /**
+     * 获取当前登录用户的 APP 购课记录详情。
+     */
+    public AppCoursePurchaseOrderDetailVO appUserDetail(String orderNo) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        CoursePurchaseOrderListRow row = coursePurchaseOrderMapper.selectAppUserOrderDetail(userId, trimToNull(orderNo));
+        if (row == null) {
+            throw new IllegalArgumentException("购课记录不存在");
+        }
+        return toAppDetail(row);
+    }
+
+    /**
+     * 导出后台养生课程购买订单。
+     */
     public void export(HttpServletResponse response,
                        String orderNo,
                        String userPhone,
@@ -117,6 +157,9 @@
         throw new IllegalArgumentException("退款功能待支付方案确认后接入");
     }
 
+    /**
+     * 删除后台已退款养生课程购买订单。
+     */
     @Transactional
     public void delete(Long id) {
         CoursePurchaseOrder order = requireOrder(id);
@@ -126,6 +169,9 @@
         coursePurchaseOrderMapper.deleteById(id);
     }
 
+    /**
+     * 校验养生课程购买订单存在。
+     */
     private CoursePurchaseOrder requireOrder(Long id) {
         CoursePurchaseOrder order = coursePurchaseOrderMapper.selectOne(new LambdaQueryWrapper<CoursePurchaseOrder>()
                 .eq(CoursePurchaseOrder::getId, id)
@@ -138,6 +184,9 @@
         return order;
     }
 
+    /**
+     * 将后台订单查询结果转换为后台列表响应。
+     */
     private List<CoursePurchaseOrderListItemVO> toList(List<CoursePurchaseOrderListRow> rows) {
         List<CoursePurchaseOrderListItemVO> list = new ArrayList<CoursePurchaseOrderListItemVO>();
         for (CoursePurchaseOrderListRow row : rows) {
@@ -158,6 +207,62 @@
         return list;
     }
 
+    /**
+     * 将订单查询结果转换为 APP 购课记录列表响应。
+     */
+    private List<AppCoursePurchaseOrderItemVO> toAppList(List<CoursePurchaseOrderListRow> rows) {
+        List<AppCoursePurchaseOrderItemVO> list = new ArrayList<AppCoursePurchaseOrderItemVO>();
+        for (CoursePurchaseOrderListRow row : rows) {
+            AppCoursePurchaseOrderItemVO item = new AppCoursePurchaseOrderItemVO();
+            item.setId(row.getId());
+            item.setOrderNo(row.getOrderNo());
+            item.setCourseId(row.getCourseId());
+            item.setCourseVideoId(row.getCourseVideoId());
+            item.setProductName(row.getProductName());
+            item.setCourseTitle(row.getCourseTitle());
+            item.setChapterName(row.getChapterName());
+            item.setCoverImage(row.getCoverImage());
+            item.setOrderAmount(row.getOrderAmount());
+            item.setPaymentMethod(row.getPaymentMethod());
+            item.setPaymentMethodText(paymentMethodText(row.getPaymentMethod()));
+            item.setStatus(row.getStatus());
+            // APP 购课记录页只需标记是否退款,避免前端重复判断订单状态。
+            item.setShowRefundTag(Boolean.valueOf(STATUS_REFUNDED.equals(row.getStatus())));
+            item.setPaidAt(row.getPaidAt());
+            list.add(item);
+        }
+        return list;
+    }
+
+    /**
+     * 将订单详情查询结果转换为 APP 购课记录详情响应。
+     */
+    private AppCoursePurchaseOrderDetailVO toAppDetail(CoursePurchaseOrderListRow row) {
+        AppCoursePurchaseOrderDetailVO detail = new AppCoursePurchaseOrderDetailVO();
+        detail.setId(row.getId());
+        detail.setOrderNo(row.getOrderNo());
+        detail.setCourseId(row.getCourseId());
+        detail.setCourseVideoId(row.getCourseVideoId());
+        detail.setProductName(row.getProductName());
+        detail.setCourseTitle(row.getCourseTitle());
+        detail.setChapterName(row.getChapterName());
+        detail.setCoverImage(row.getCoverImage());
+        detail.setOrderAmount(row.getOrderAmount());
+        detail.setPaymentMethod(row.getPaymentMethod());
+        detail.setPaymentMethodText(paymentMethodText(row.getPaymentMethod()));
+        detail.setStatus(row.getStatus());
+        // 详情页退款信息直接由后端归一化,避免前端重复拼装退款状态。
+        detail.setRefundAmount(resolveRefundAmount(row.getStatus(), row.getOrderAmount(), row.getRefundAmount()));
+        detail.setShowRefundTag(Boolean.valueOf(STATUS_REFUNDED.equals(row.getStatus())));
+        detail.setCreatedAt(row.getCreatedAt());
+        detail.setPaidAt(row.getPaidAt());
+        detail.setRefundedAt(row.getRefundedAt());
+        return detail;
+    }
+
+    /**
+     * 解析订单应展示的退款金额。
+     */
     private BigDecimal resolveRefundAmount(String status, BigDecimal orderAmount, BigDecimal refundAmount) {
         if (!STATUS_REFUNDED.equals(status)) {
             return null;
@@ -165,6 +270,9 @@
         return refundAmount == null ? orderAmount : refundAmount;
     }
 
+    /**
+     * 归一化后台筛选状态。
+     */
     private String normalizeStatus(String status) {
         String normalized = trimToNull(status);
         if (normalized == null) {
@@ -176,6 +284,9 @@
         return null;
     }
 
+    /**
+     * 将空白字符串归一化为 null。
+     */
     private String trimToNull(String value) {
         if (value == null) {
             return null;
@@ -184,10 +295,16 @@
         return trimmed.isEmpty() ? null : trimmed;
     }
 
+    /**
+     * 缺省文本兜底。
+     */
     private String defaultText(String value) {
         return value == null || value.trim().isEmpty() ? "-" : value.trim();
     }
 
+    /**
+     * 解析支付方式展示文案。
+     */
     private String paymentMethodText(String paymentMethod) {
         if (PAYMENT_METHOD_WECHAT.equals(paymentMethod)) {
             return "微信支付";
@@ -198,6 +315,9 @@
         return "-";
     }
 
+    /**
+     * 解析订单状态展示文案。
+     */
     private String statusText(String status) {
         if (STATUS_WAITING_PAYMENT.equals(status)) {
             return "待支付";
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/CoursePurchaseOrderListRow.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/CoursePurchaseOrderListRow.java
index bf4602b..8caf6b9 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/CoursePurchaseOrderListRow.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/live/vo/CoursePurchaseOrderListRow.java
@@ -14,13 +14,20 @@
 
     private Long id;
     private String orderNo;
+    private Long courseId;
+    private Long courseVideoId;
     private String userNickname;
     private String userPhone;
     private String productName;
+    private String courseTitle;
+    private String chapterName;
+    private String coverImage;
     private BigDecimal orderAmount;
     private String paymentMethod;
     private BigDecimal refundAmount;
     private String refundRemark;
     private String status;
+    private LocalDateTime paidAt;
+    private LocalDateTime refundedAt;
     private LocalDateTime createdAt;
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/MemberPurchaseOrderMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/MemberPurchaseOrderMapper.java
index 3565db0..07ee83b 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/MemberPurchaseOrderMapper.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/mapper/MemberPurchaseOrderMapper.java
@@ -29,4 +29,7 @@
                                                  @Param("status") String status,
                                                  @Param("createdFrom") LocalDateTime createdFrom,
                                                  @Param("createdTo") LocalDateTime createdTo);
+
+    IPage<MemberPurchaseOrderRow> selectAppUserOrderPage(IPage<MemberPurchaseOrderRow> page,
+                                                         @Param("appUserId") Long appUserId);
 }
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/MemberPurchaseOrderAppService.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/MemberPurchaseOrderAppService.java
index eb886b5..649b403 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/MemberPurchaseOrderAppService.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/service/MemberPurchaseOrderAppService.java
@@ -4,6 +4,8 @@
 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.app.auth.service.AppAuthSessionService;
+import com.tcm.training.admin.app.member.vo.AppMemberPurchaseOrderItemVO;
 import com.tcm.training.admin.common.DateTimes;
 import com.tcm.training.admin.common.PageResponse;
 import com.tcm.training.admin.common.PhoneMasker;
@@ -37,11 +39,20 @@
     private static final String PAYMENT_METHOD_ALIPAY = "ALIPAY";
 
     private final MemberPurchaseOrderMapper memberPurchaseOrderMapper;
+    private final AppAuthSessionService appAuthSessionService;
 
-    public MemberPurchaseOrderAppService(MemberPurchaseOrderMapper memberPurchaseOrderMapper) {
+    /**
+     * 注入会员订单查询能力。
+     */
+    public MemberPurchaseOrderAppService(MemberPurchaseOrderMapper memberPurchaseOrderMapper,
+                                         AppAuthSessionService appAuthSessionService) {
         this.memberPurchaseOrderMapper = memberPurchaseOrderMapper;
+        this.appAuthSessionService = appAuthSessionService;
     }
 
+    /**
+     * 分页查询后台会员购买订单。
+     */
     public PageResponse<MemberPurchaseOrderListItemVO> page(int page,
                                                             int pageSize,
                                                             String orderNo,
@@ -64,6 +75,21 @@
         return PageResponse.of(toList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize());
     }
 
+    /**
+     * 分页查询当前登录用户的 APP 会员购买记录。
+     */
+    public PageResponse<AppMemberPurchaseOrderItemVO> appUserPage(int page, int pageSize) {
+        Long userId = appAuthSessionService.requireLoginUserId();
+        IPage<MemberPurchaseOrderRow> result = memberPurchaseOrderMapper.selectAppUserOrderPage(
+                new Page<MemberPurchaseOrderRow>(page, pageSize),
+                userId
+        );
+        return PageResponse.of(toAppList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize());
+    }
+
+    /**
+     * 导出后台会员购买订单。
+     */
     public void export(String orderNo,
                        String userPhone,
                        String memberType,
@@ -116,6 +142,9 @@
         throw new IllegalArgumentException("退款功能待支付方案确认后接入");
     }
 
+    /**
+     * 删除后台已退款会员订单。
+     */
     @Transactional
     public void delete(Long id) {
         MemberPurchaseOrder order = requireOrder(id);
@@ -125,6 +154,9 @@
         memberPurchaseOrderMapper.deleteById(id);
     }
 
+    /**
+     * 校验会员购买订单存在。
+     */
     private MemberPurchaseOrder requireOrder(Long id) {
         MemberPurchaseOrder order = memberPurchaseOrderMapper.selectOne(new LambdaQueryWrapper<MemberPurchaseOrder>()
                 .eq(MemberPurchaseOrder::getId, id)
@@ -136,6 +168,9 @@
         return order;
     }
 
+    /**
+     * 将后台订单查询结果转换为后台列表响应。
+     */
     private List<MemberPurchaseOrderListItemVO> toList(List<MemberPurchaseOrderRow> rows) {
         List<MemberPurchaseOrderListItemVO> list = new ArrayList<MemberPurchaseOrderListItemVO>();
         for (MemberPurchaseOrderRow row : rows) {
@@ -159,6 +194,33 @@
         return list;
     }
 
+    /**
+     * 将订单查询结果转换为 APP 购买记录列表响应。
+     */
+    private List<AppMemberPurchaseOrderItemVO> toAppList(List<MemberPurchaseOrderRow> rows) {
+        List<AppMemberPurchaseOrderItemVO> list = new ArrayList<AppMemberPurchaseOrderItemVO>();
+        for (MemberPurchaseOrderRow row : rows) {
+            AppMemberPurchaseOrderItemVO item = new AppMemberPurchaseOrderItemVO();
+            item.setId(row.getId());
+            item.setOrderNo(row.getOrderNo());
+            item.setMemberType(row.getMemberType());
+            item.setMemberTypeText(memberTypeText(row.getMemberType()));
+            item.setOrderAmount(row.getOrderAmount());
+            item.setPaymentMethod(row.getPaymentMethod());
+            item.setPaymentMethodText(paymentMethodText(row.getPaymentMethod()));
+            item.setStatus(row.getStatus());
+            // APP 购买记录页在退款场景下要展示退款金额标签,没有单独退款额时回退订单金额。
+            item.setRefundAmount(resolveRefundAmount(row.getStatus(), row.getOrderAmount(), row.getRefundAmount()));
+            item.setShowRefundTag(Boolean.valueOf(STATUS_REFUNDED.equals(row.getStatus())));
+            item.setPaidAt(row.getPaidAt());
+            list.add(item);
+        }
+        return list;
+    }
+
+    /**
+     * 解析订单应展示的退款金额。
+     */
     private BigDecimal resolveRefundAmount(String status, BigDecimal orderAmount, BigDecimal refundAmount) {
         if (!STATUS_REFUNDED.equals(status)) {
             return null;
@@ -177,6 +239,9 @@
         return null;
     }
 
+    /**
+     * 将空白文本归一化为 null。
+     */
     private String trimToNull(String value) {
         if (value == null) {
             return null;
@@ -185,24 +250,36 @@
         return trimmed.isEmpty() ? null : trimmed;
     }
 
+    /**
+     * 缺省文本展示兜底。
+     */
     private String defaultText(String value) {
         return value == null || value.trim().isEmpty() ? "-" : value.trim();
     }
 
+    /**
+     * 将会员天数转换为展示文案。
+     */
     private String memberDaysText(Integer memberDays) {
         return memberDays == null ? "-" : memberDays + "天";
     }
 
+    /**
+     * 将会员类型转换为展示文案。
+     */
     private String memberTypeText(String memberType) {
         if (MEMBER_TYPE_MONTH_CARD.equals(memberType)) {
             return "月卡";
         }
         if (MEMBER_TYPE_QUARTER_CARD.equals(memberType)) {
-            return "季卡";
+            return "季度会员";
         }
         return "-";
     }
 
+    /**
+     * 将支付方式转换为展示文案。
+     */
     private String paymentMethodText(String paymentMethod) {
         if (PAYMENT_METHOD_WECHAT.equals(paymentMethod)) {
             return "微信支付";
@@ -213,6 +290,9 @@
         return "-";
     }
 
+    /**
+     * 将订单状态转换为展示文案。
+     */
     private String statusText(String status) {
         if (STATUS_WAITING_PAYMENT.equals(status)) {
             return "待支付";
diff --git a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/mapper/PointsLedgerMapper.java b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/mapper/PointsLedgerMapper.java
index 5905a76..e64c223 100644
--- a/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/mapper/PointsLedgerMapper.java
+++ b/tcm-training-admin-backend/src/main/java/com/tcm/training/admin/points/mapper/PointsLedgerMapper.java
@@ -28,6 +28,10 @@
                                             @Param("operatedFrom") LocalDateTime operatedFrom,
                                             @Param("operatedTo") LocalDateTime operatedTo);
 
+    IPage<PointsLedgerRow> selectAppLedgerPage(IPage<PointsLedgerRow> page,
+                                               @Param("appUserId") Long appUserId,
+                                               @Param("incomeExpenseType") String incomeExpenseType);
+
     List<PointsLedgerRow> selectLedgerList(@Param("userPhone") String userPhone,
                                            @Param("regionCode") String regionCode,
                                            @Param("clinicName") String clinicName,
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/CoursePurchaseOrderMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/CoursePurchaseOrderMapper.xml
index c5f299c..3c690cb 100644
--- a/tcm-training-admin-backend/src/main/resources/mapper/CoursePurchaseOrderMapper.xml
+++ b/tcm-training-admin-backend/src/main/resources/mapper/CoursePurchaseOrderMapper.xml
@@ -34,36 +34,138 @@
         SELECT
             o.id,
             o.order_no AS orderNo,
+            o.course_id AS courseId,
+            o.course_video_id AS courseVideoId,
             o.user_nickname AS userNickname,
             o.user_phone AS userPhone,
             o.product_name_snapshot AS productName,
+            o.course_title_snapshot AS courseTitle,
+            o.chapter_name_snapshot AS chapterName,
+            o.cover_img_snapshot AS coverImage,
             o.order_amount AS orderAmount,
             o.payment_method AS paymentMethod,
             o.refund_amount AS refundAmount,
             o.refund_remark AS refundRemark,
             o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
             o.created_at AS createdAt
         FROM tcm_course_purchase_order o
         <include refid="CoursePurchaseOrderWhere"/>
         ORDER BY o.id DESC
     </select>
 
-    <select id="selectOrderList" resultType="com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow">
+    <select id="selectAppUserOrderPage" resultType="com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow">
         SELECT
             o.id,
             o.order_no AS orderNo,
+            o.course_id AS courseId,
+            o.course_video_id AS courseVideoId,
             o.user_nickname AS userNickname,
             o.user_phone AS userPhone,
             o.product_name_snapshot AS productName,
+            o.course_title_snapshot AS courseTitle,
+            o.chapter_name_snapshot AS chapterName,
+            o.cover_img_snapshot AS coverImage,
             o.order_amount AS orderAmount,
             o.payment_method AS paymentMethod,
             o.refund_amount AS refundAmount,
             o.refund_remark AS refundRemark,
             o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
+            o.created_at AS createdAt
+        FROM tcm_course_purchase_order o
+        WHERE o.deleted = 0
+        AND o.course_type = 'HEALTH_COURSE'
+        AND o.status IN ('PAID', 'REFUNDED')
+        AND o.app_user_id = #{appUserId}
+        <if test="productName != null and productName.trim() != ''">
+            AND o.product_name_snapshot LIKE CONCAT('%', #{productName}, '%')
+        </if>
+        ORDER BY o.id DESC
+    </select>
+
+    <select id="selectAppUserOrderDetail" resultType="com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow">
+        SELECT
+            o.id,
+            o.order_no AS orderNo,
+            o.course_id AS courseId,
+            o.course_video_id AS courseVideoId,
+            o.user_nickname AS userNickname,
+            o.user_phone AS userPhone,
+            o.product_name_snapshot AS productName,
+            o.course_title_snapshot AS courseTitle,
+            o.chapter_name_snapshot AS chapterName,
+            o.cover_img_snapshot AS coverImage,
+            o.order_amount AS orderAmount,
+            o.payment_method AS paymentMethod,
+            o.refund_amount AS refundAmount,
+            o.refund_remark AS refundRemark,
+            o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
+            o.created_at AS createdAt
+        FROM tcm_course_purchase_order o
+        WHERE o.deleted = 0
+        AND o.course_type = 'HEALTH_COURSE'
+        AND o.app_user_id = #{appUserId}
+        AND o.order_no = #{orderNo}
+        LIMIT 1
+    </select>
+
+    <select id="selectOrderList" resultType="com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow">
+        SELECT
+            o.id,
+            o.order_no AS orderNo,
+            o.course_id AS courseId,
+            o.course_video_id AS courseVideoId,
+            o.user_nickname AS userNickname,
+            o.user_phone AS userPhone,
+            o.product_name_snapshot AS productName,
+            o.course_title_snapshot AS courseTitle,
+            o.chapter_name_snapshot AS chapterName,
+            o.cover_img_snapshot AS coverImage,
+            o.order_amount AS orderAmount,
+            o.payment_method AS paymentMethod,
+            o.refund_amount AS refundAmount,
+            o.refund_remark AS refundRemark,
+            o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
             o.created_at AS createdAt
         FROM tcm_course_purchase_order o
         <include refid="CoursePurchaseOrderWhere"/>
         ORDER BY o.id DESC
     </select>
 
+    <select id="selectPaidCourseSnapshots" resultType="com.tcm.training.admin.live.vo.CoursePurchaseOrderListRow">
+        SELECT
+            o.id,
+            o.order_no AS orderNo,
+            o.course_id AS courseId,
+            o.course_video_id AS courseVideoId,
+            o.user_nickname AS userNickname,
+            o.user_phone AS userPhone,
+            o.product_name_snapshot AS productName,
+            o.course_title_snapshot AS courseTitle,
+            o.chapter_name_snapshot AS chapterName,
+            o.cover_img_snapshot AS coverImage,
+            o.order_amount AS orderAmount,
+            o.payment_method AS paymentMethod,
+            o.refund_amount AS refundAmount,
+            o.refund_remark AS refundRemark,
+            o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
+            o.created_at AS createdAt
+        FROM tcm_course_purchase_order o
+        WHERE o.deleted = 0
+          AND o.course_type = 'HEALTH_COURSE'
+          AND o.purchase_target_type = 'COURSE'
+          AND o.status = 'PAID'
+          AND o.app_user_id = #{appUserId}
+        ORDER BY o.paid_at DESC, o.id DESC
+    </select>
+
 </mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseDownloadRecordMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseDownloadRecordMapper.xml
new file mode 100644
index 0000000..36a9b07
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseDownloadRecordMapper.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.tcm.training.admin.live.mapper.HealthCourseDownloadRecordMapper">
+
+    <select id="selectAppDownloads" resultType="com.tcm.training.admin.live.row.AppHealthCourseDownloadRow">
+        SELECT
+            r.id AS id,
+            r.course_id AS courseId,
+            r.course_video_id AS courseVideoId,
+            r.chapter_name_snapshot AS title,
+            r.course_title_snapshot AS subtitle,
+            r.cover_img_snapshot AS coverImage,
+            r.duration_seconds_snapshot AS durationSeconds,
+            r.video_url_snapshot AS videoUrl,
+            r.updated_at AS downloadedAt
+        FROM tcm_health_course_download_record r
+        WHERE r.deleted = 0
+          AND r.app_user_id = #{appUserId}
+        ORDER BY r.updated_at DESC, r.id DESC
+    </select>
+
+</mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseFavoriteRecordMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseFavoriteRecordMapper.xml
new file mode 100644
index 0000000..61cb77c
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseFavoriteRecordMapper.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.tcm.training.admin.live.mapper.HealthCourseFavoriteRecordMapper">
+
+    <select id="selectAppFavoriteCourses" resultType="com.tcm.training.admin.live.row.AppHealthCourseFavoriteRow">
+        SELECT
+            latest.course_id AS courseId,
+            c.title AS title,
+            c.cover_img AS coverImage,
+            lc.name AS levelName,
+            dc.name AS difficultyName,
+            e.name AS teacherName,
+            e.avatar AS teacherAvatar,
+            c.learn_user_count AS learnUserCount,
+            latest.action_time AS favoritedAt
+        FROM (
+            SELECT r.course_id, r.action_time, r.id
+            FROM tcm_health_course_favorite_record r
+            INNER JOIN (
+                SELECT course_id, MAX(id) AS max_id
+                FROM tcm_health_course_favorite_record
+                WHERE app_user_id = #{appUserId}
+                GROUP BY course_id
+            ) latest_record ON latest_record.max_id = r.id
+            WHERE r.app_user_id = #{appUserId}
+            AND r.action_type = 'FAVORITE'
+        ) latest
+        INNER JOIN tcm_health_course c ON c.id = latest.course_id
+            AND c.deleted = 0
+            AND c.status = 1
+        LEFT JOIN tcm_live_course_category lc ON lc.id = c.level_category_id AND lc.deleted = 0
+        LEFT JOIN tcm_live_course_category dc ON dc.id = c.difficulty_category_id AND dc.deleted = 0
+        LEFT JOIN tcm_expert e ON e.id = c.expert_id AND e.deleted = 0
+        ORDER BY latest.action_time DESC, latest.id DESC
+    </select>
+
+</mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseMemberJoinRecordMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseMemberJoinRecordMapper.xml
new file mode 100644
index 0000000..9d6ace8
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/resources/mapper/HealthCourseMemberJoinRecordMapper.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper">
+
+    <select id="selectActiveCourseIdsByUserId" resultType="java.lang.Long">
+        SELECT DISTINCT r.course_id
+        FROM tcm_health_course_member_join_record r
+        WHERE r.app_user_id = #{appUserId}
+          AND r.deleted = 0
+        ORDER BY r.joined_at DESC, r.id DESC
+    </select>
+
+</mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/HealthCoursePlayRecordMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/HealthCoursePlayRecordMapper.xml
new file mode 100644
index 0000000..6f6d4b4
--- /dev/null
+++ b/tcm-training-admin-backend/src/main/resources/mapper/HealthCoursePlayRecordMapper.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.tcm.training.admin.live.mapper.HealthCoursePlayRecordMapper">
+
+    <select id="selectVideoProgressByCourseIds" resultType="com.tcm.training.admin.live.row.AppMyHealthCourseVideoProgressRow">
+        SELECT
+            p.course_id AS courseId,
+            p.video_id AS videoId,
+            COALESCE(SUM(p.duration_seconds), 0) AS cumulativeWatchSeconds,
+            MAX(p.is_completed) AS completed,
+            SUBSTRING_INDEX(
+                GROUP_CONCAT(
+                    COALESCE(p.last_play_position_seconds, 0)
+                    ORDER BY COALESCE(p.play_end, p.play_start) DESC, p.id DESC
+                ),
+                ',',
+                1
+            ) AS lastPlayPositionSeconds,
+            MAX(COALESCE(p.play_end, p.play_start)) AS latestLearnAt
+        FROM tcm_health_course_play_record p
+        WHERE p.app_user_id = #{appUserId}
+          AND p.course_id IN
+          <foreach collection="courseIds" item="courseId" open="(" separator="," close=")">
+              #{courseId}
+          </foreach>
+        GROUP BY p.course_id, p.video_id
+    </select>
+
+    <select id="selectCourseProgressByCourseIds" resultType="com.tcm.training.admin.live.row.AppMyHealthCourseProgressRow">
+        SELECT
+            s.course_id AS courseId,
+            COUNT(CASE WHEN s.completed = 1 THEN 1 END) AS completedLessons,
+            MAX(s.latestLearnAt) AS latestLearnAt
+        FROM (
+            SELECT
+                p.course_id,
+                p.video_id,
+                MAX(p.is_completed) AS completed,
+                MAX(COALESCE(p.play_end, p.play_start)) AS latestLearnAt
+            FROM tcm_health_course_play_record p
+            WHERE p.app_user_id = #{appUserId}
+              AND p.course_id IN
+              <foreach collection="courseIds" item="courseId" open="(" separator="," close=")">
+                  #{courseId}
+              </foreach>
+            GROUP BY p.course_id, p.video_id
+        ) s
+        GROUP BY s.course_id
+    </select>
+
+</mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/MemberPurchaseOrderMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/MemberPurchaseOrderMapper.xml
index 4863846..f6ec896 100644
--- a/tcm-training-admin-backend/src/main/resources/mapper/MemberPurchaseOrderMapper.xml
+++ b/tcm-training-admin-backend/src/main/resources/mapper/MemberPurchaseOrderMapper.xml
@@ -71,4 +71,27 @@
         ORDER BY o.id DESC
     </select>
 
+    <select id="selectAppUserOrderPage" resultType="com.tcm.training.admin.member.row.MemberPurchaseOrderRow">
+        SELECT
+            o.id,
+            o.order_no AS orderNo,
+            o.user_nickname AS userNickname,
+            o.user_phone AS userPhone,
+            o.member_type AS memberType,
+            o.member_days AS memberDays,
+            o.order_amount AS orderAmount,
+            o.payment_method AS paymentMethod,
+            o.refund_amount AS refundAmount,
+            o.refund_remark AS refundRemark,
+            o.status,
+            o.paid_at AS paidAt,
+            o.refunded_at AS refundedAt,
+            o.created_at AS createdAt
+        FROM tcm_member_purchase_order o
+        WHERE o.deleted = 0
+        AND o.app_user_id = #{appUserId}
+        AND o.status IN ('PAID', 'REFUNDED')
+        ORDER BY o.id DESC
+    </select>
+
 </mapper>
diff --git a/tcm-training-admin-backend/src/main/resources/mapper/PointsLedgerMapper.xml b/tcm-training-admin-backend/src/main/resources/mapper/PointsLedgerMapper.xml
index ad705c9..1abbc80 100644
--- a/tcm-training-admin-backend/src/main/resources/mapper/PointsLedgerMapper.xml
+++ b/tcm-training-admin-backend/src/main/resources/mapper/PointsLedgerMapper.xml
@@ -80,6 +80,18 @@
         ORDER BY l.operated_at DESC, l.id DESC
     </select>
 
+    <select id="selectAppLedgerPage" resultType="com.tcm.training.admin.points.row.PointsLedgerRow">
+        SELECT
+            <include refid="LedgerColumns"/>
+        FROM tcm_points_ledger l
+        WHERE l.deleted = 0
+          AND l.app_user_id = #{appUserId}
+        <if test="incomeExpenseType != null and incomeExpenseType.trim() != ''">
+            AND l.income_expense_type = #{incomeExpenseType}
+        </if>
+        ORDER BY l.operated_at DESC, l.id DESC
+    </select>
+
     <select id="selectLedgerList" resultType="com.tcm.training.admin.points.row.PointsLedgerRow">
         SELECT
             <include refid="LedgerColumns"/>
diff --git a/traditionalApp/pages/courseDetail/courseDetail.vue b/traditionalApp/pages/courseDetail/courseDetail.vue
index 7301e31..d2831b4 100644
--- a/traditionalApp/pages/courseDetail/courseDetail.vue
+++ b/traditionalApp/pages/courseDetail/courseDetail.vue
@@ -279,8 +279,10 @@
 	memberJoinHealthCourse,
 	purchaseHealthCourse,
 	purchaseHealthCourseVideo,
+	reportHealthCourseDownload,
 	unfavoriteHealthCourse,
 } from './service'
+import { clearDownloadTask, listDownloadTasks, upsertDownloadTask } from '@/utils/download-task-storage.js'
 
 interface CourseExpert {
 	id?: number | null
@@ -299,6 +301,21 @@
 	purchased: boolean
 	locked: boolean
 	videoUrl?: string
+}
+
+interface LocalDownloadTask {
+	clientTaskId: string
+	courseId: number
+	courseVideoId: number
+	title: string
+	subtitle: string
+	coverImage: string
+	durationSeconds: number
+	durationText: string
+	videoUrl: string
+	progress: number
+	status: 'downloading'
+	updatedAt: string
 }
 
 interface CourseDetail {
@@ -672,6 +689,55 @@
 }
 
 /**
+ * 读取当前缓存中的 APP 用户标识,供本地下载任务按账号隔离。
+ */
+function resolveCurrentUserId() {
+	const cachedUser = uni.getStorageSync('appUserInfo')
+	if (!cachedUser || typeof cachedUser !== 'object') {
+		return 0
+	}
+	const userId = Number((cachedUser as { id?: number | string }).id || 0)
+	if (!Number.isFinite(userId) || userId <= 0) {
+		return 0
+	}
+	return userId
+}
+
+/**
+ * 判断指定章节是否已存在下载中任务,避免重复触发原生下载。
+ */
+function hasActiveDownloadTask(userId: number, courseVideoId: number) {
+	return listDownloadTasks(userId).some((task) => Number(task.courseVideoId) === Number(courseVideoId))
+}
+
+/**
+ * 构建课程下载任务唯一标识。
+ */
+function buildDownloadTaskId(courseVideoId: number) {
+	return `health-course-video-${courseVideoId}`
+}
+
+/**
+ * 组装下载中任务的本地快照,供“我的下载”页面直接复用展示。
+ */
+function createDownloadTaskPayload(clientTaskId: string, item: CourseVideo): LocalDownloadTask {
+	return {
+		clientTaskId,
+		courseId: Number(courseId.value || 0),
+		courseVideoId: Number(item.id || 0),
+		title: item.chapterName || '课程章节',
+		subtitle: courseDetail.value.title || '养生课程',
+		coverImage: swiperList.value[0] || '/static/image/home/class.png',
+		durationSeconds: Number(item.durationSeconds || 0),
+		durationText: item.durationText || '00:00',
+		videoUrl: item.videoUrl || '',
+		progress: 0,
+		status: 'downloading',
+		updatedAt: new Date().toISOString(),
+	}
+}
+
+/**
  * 打开整课购买支付弹窗。
  */
 function handleCoursePurchase() {
@@ -722,14 +788,33 @@
 	})
 	return
 	// #endif
+	const currentUserId = resolveCurrentUserId()
+	if (!currentUserId) {
+		uni.showToast({
+			title: '请先登录',
+			icon: 'none',
+		})
+		return
+	}
+	if (hasActiveDownloadTask(currentUserId, item.id)) {
+		uni.showToast({
+			title: '当前章节正在下载中',
+			icon: 'none',
+		})
+		return
+	}
+	const downloadTaskId = buildDownloadTaskId(item.id)
+	const initialTask = createDownloadTaskPayload(downloadTaskId, item)
+	upsertDownloadTask(currentUserId, initialTask)
 	uni.showLoading({
 		title: '下载中...',
 		mask: true,
 	})
-	uni.downloadFile({
+	const downloadTask = uni.downloadFile({
 		url: item.videoUrl || '',
 		success: (downloadResult) => {
 			if (downloadResult.statusCode !== 200 || !downloadResult.tempFilePath) {
+				clearDownloadTask(currentUserId, downloadTaskId)
 				uni.showToast({
 					title: '下载失败',
 					icon: 'none',
@@ -739,13 +824,30 @@
 			// 先持久化保存,避免临时文件被系统回收后用户无法继续使用。
 			uni.saveFile({
 				tempFilePath: downloadResult.tempFilePath,
-				success: () => {
-					uni.showToast({
-						title: '下载完成',
-						icon: 'none',
-					})
+				success: async () => {
+					// 文件已持久化后再移除下载中状态,并把完成记录同步给后端列表。
+					clearDownloadTask(currentUserId, downloadTaskId)
+					try {
+						const response = await reportHealthCourseDownload({
+							courseId: Number(courseId.value),
+							courseVideoId: item.id,
+						})
+						if (response?.code !== 0) {
+							return
+						}
+						uni.showToast({
+							title: '下载完成',
+							icon: 'none',
+						})
+					} catch {
+						uni.showToast({
+							title: '下载完成,记录同步失败',
+							icon: 'none',
+						})
+					}
 				},
 				fail: () => {
+					clearDownloadTask(currentUserId, downloadTaskId)
 					uni.showToast({
 						title: '文件已下载到临时目录',
 						icon: 'none',
@@ -754,6 +856,7 @@
 			})
 		},
 		fail: () => {
+			clearDownloadTask(currentUserId, downloadTaskId)
 			uni.showToast({
 				title: '下载失败',
 				icon: 'none',
@@ -763,6 +866,14 @@
 			uni.hideLoading()
 		},
 	})
+	// 下载中的进度仅在当前设备维护,避免把临时任务状态同步到服务端。
+	downloadTask.onProgressUpdate((progressEvent) => {
+		upsertDownloadTask(currentUserId, {
+			...initialTask,
+			progress: Number(progressEvent?.progress || 0),
+			updatedAt: new Date().toISOString(),
+		})
+	})
 }
 
 /**
diff --git a/traditionalApp/pages/courseDetail/service.js b/traditionalApp/pages/courseDetail/service.js
index 9bfb560..598318e 100644
--- a/traditionalApp/pages/courseDetail/service.js
+++ b/traditionalApp/pages/courseDetail/service.js
@@ -43,6 +43,13 @@
 }
 
 /**
+ * 上报 APP 养生课程小节下载完成记录。
+ */
+export const reportHealthCourseDownload = (data) => {
+	return request.post('/api/app/health-course-downloads', data, false, false)
+}
+
+/**
  * 获取 APP 会员开通页聚合数据。
  */
 export const getMemberOpenPage = () => {
diff --git a/traditionalApp/pages/courseRecord/courseRecord.vue b/traditionalApp/pages/courseRecord/courseRecord.vue
index bf8247a..154c06e 100644
--- a/traditionalApp/pages/courseRecord/courseRecord.vue
+++ b/traditionalApp/pages/courseRecord/courseRecord.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -9,82 +8,45 @@
 			</view>
 		</view>
 
-		<!-- 搜索栏 -->
 		<view class="search-box">
 			<view class="search-left">
 				<icon type="search" size="14" color="#999"></icon>
-				<input type="text" placeholder="课程名称" placeholder-class="ph-color" class="search-input" />
+				<input
+					v-model="searchKeyword"
+					type="text"
+					placeholder="课程名称"
+					placeholder-class="ph-color"
+					class="search-input"
+					confirm-type="search"
+					@confirm="handleSearch"
+				/>
 			</view>
-			<view class="search-btn">搜索</view>
+			<view class="search-btn" @tap="handleSearch">搜索</view>
 		</view>
 
-		<!-- 记录列表 -->
 		<scroll-view scroll-y class="record-list">
-			<!-- 记录项 1 -->
-			<view class="record-item" @tap="goDetail">
-				<image class="course-img" src="/static/image/home/class.png" mode="aspectFill"></image>
-				<view class="item-info">
-					<view class="item-title">课程课程名称标题</view>
-					<view class="item-desc">课程名称课程名称课程名称</view>
-					<view class="item-bottom">
-						<text class="item-time">2025.12.20 00:20</text>
-						<view class="price-box">
-							<text class="price-symbol">¥</text>
-							<text class="price-num">8888.25</text>
-						</view>
-					</view>
-				</view>
+			<view v-if="records.length === 0" class="empty-box">
+				<text class="empty-text">暂无购买记录</text>
 			</view>
-
-			<!-- 记录项 2 (退款状态) -->
-			<view class="record-item" @tap="goDetail">
-				<image class="course-img" src="/static/image/home/class.png" mode="aspectFill"></image>
+			<view
+				v-for="item in records"
+				:key="item.id || item.orderNo"
+				class="record-item"
+				@tap="goDetail(item)"
+			>
+				<image class="course-img" :src="resolveCoverImage(item.coverImage)" mode="aspectFill"></image>
 				<view class="item-info">
-					<view class="title-row">
-						<view class="item-title">课程课程名称标题</view>
+					<view v-if="item.showRefundTag" class="title-row">
+						<view class="item-title">{{ item.productName }}</view>
 						<view class="refund-tag">退款</view>
 					</view>
-					<view class="item-desc"></view>
+					<view v-else class="item-title">{{ item.productName }}</view>
+					<view class="item-desc">{{ resolveDescription(item) }}</view>
 					<view class="item-bottom">
-						<text class="item-time">2025.12.20 00:20</text>
+						<text class="item-time">{{ formatDateTime(item.paidAt) }}</text>
 						<view class="price-box">
 							<text class="price-symbol">¥</text>
-							<text class="price-num">8888.25</text>
-						</view>
-					</view>
-				</view>
-			</view>
-
-			<!-- 记录项 3 -->
-			<view class="record-item">
-				<image class="course-img" src="/static/image/home/class.png" mode="aspectFill"></image>
-				<view class="item-info">
-					<view class="item-title">课程课程名称标题</view>
-					<view class="item-desc">课程名称课程名称课程名称</view>
-					<view class="item-bottom">
-						<text class="item-time">2025.12.20 00:20</text>
-						<view class="price-box">
-							<text class="price-symbol">¥</text>
-							<text class="price-num">8888.25</text>
-						</view>
-					</view>
-				</view>
-			</view>
-
-			<!-- 记录项 4 (退款状态) -->
-			<view class="record-item">
-				<image class="course-img" src="/static/image/home/class.png" mode="aspectFill"></image>
-				<view class="item-info">
-					<view class="title-row">
-						<view class="item-title">课程课程名称标题</view>
-						<view class="refund-tag">退款</view>
-					</view>
-					<view class="item-desc"></view>
-					<view class="item-bottom">
-						<text class="item-time">2025.12.20 00:20</text>
-						<view class="price-box">
-							<text class="price-symbol">¥</text>
-							<text class="price-num">8888.25</text>
+							<text class="price-num">{{ formatPrice(item.orderAmount) }}</text>
 						</view>
 					</view>
 				</view>
@@ -94,210 +56,374 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { getCoursePurchaseOrders } from './service'
 
-	const statusBarHeight = ref(20);
+interface CoursePurchaseOrderItem {
+	id?: number | null
+	orderNo?: string
+	courseId?: number | null
+	courseVideoId?: number | null
+	productName?: string
+	courseTitle?: string
+	chapterName?: string
+	coverImage?: string
+	orderAmount?: number | string | null
+	paymentMethod?: string
+	paymentMethodText?: string
+	status?: string
+	showRefundTag?: boolean
+	paidAt?: string
+}
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
 
-	const goBack = () => {
-		uni.navigateBack();
-	};
+const statusBarHeight = ref(20)
+const searchKeyword = ref('')
+const records = ref<CoursePurchaseOrderItem[]>([])
 
-	const goDetail = () => {
-		uni.navigateTo({
-			url: '/pages/courseRecordDetail/courseRecordDetail'
-		});
-	};
+/**
+ * 页面加载时初始化状态栏高度。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+})
+
+/**
+ * 页面回显时刷新购课记录列表。
+ */
+onShow(() => {
+	loadCoursePurchaseOrders()
+})
+
+/**
+ * 拉取当前登录用户的购课记录第一页。
+ */
+async function loadCoursePurchaseOrders() {
+	try {
+		const response = await getCoursePurchaseOrders({
+			page: 1,
+			pageSize: 20,
+			productName: normalizeOptionalText(searchKeyword.value),
+		})
+		if (response?.code !== 0) {
+			return
+		}
+		records.value = normalizeRecords(response?.data?.list)
+	} catch {
+		// 请求层会统一处理未登录跳转和异常提示,这里不重复 toast。
+	}
+}
+
+/**
+ * 点击搜索时按课程名称刷新列表。
+ */
+function handleSearch() {
+	loadCoursePurchaseOrders()
+}
+
+/**
+ * 将接口返回的记录列表归一化为页面渲染结构。
+ */
+function normalizeRecords(value: unknown): CoursePurchaseOrderItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const record = (item && typeof item === 'object' ? item : {}) as Partial<CoursePurchaseOrderItem>
+		return {
+			id: Number.isFinite(Number(record.id)) ? Number(record.id) : null,
+			orderNo: normalizeOptionalText(record.orderNo),
+			courseId: normalizeOptionalNumber(record.courseId),
+			courseVideoId: normalizeOptionalNumber(record.courseVideoId),
+			productName: normalizeOptionalText(record.productName) || '课程名称',
+			courseTitle: normalizeOptionalText(record.courseTitle),
+			chapterName: normalizeOptionalText(record.chapterName),
+			coverImage: normalizeOptionalText(record.coverImage),
+			orderAmount: record.orderAmount ?? null,
+			paymentMethod: normalizeOptionalText(record.paymentMethod),
+			paymentMethodText: normalizeOptionalText(record.paymentMethodText),
+			status: normalizeOptionalText(record.status),
+			showRefundTag: !!record.showRefundTag,
+			paidAt: normalizeOptionalText(record.paidAt),
+		}
+	})
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 跳转到现有购课记录详情页。
+ */
+function goDetail(item: CoursePurchaseOrderItem) {
+	const orderNo = encodeURIComponent(normalizeOptionalText(item.orderNo))
+	uni.navigateTo({
+		url: `/pages/courseRecordDetail/courseRecordDetail?orderNo=${orderNo}`,
+	})
+}
+
+/**
+ * 解析课程封面图,避免接口为空时页面出现空白。
+ */
+function resolveCoverImage(value?: string) {
+	return normalizeOptionalText(value) || DEFAULT_COURSE_IMAGE
+}
+
+/**
+ * 解析列表副标题,优先展示小节名称,没有小节时展示课程名称。
+ */
+function resolveDescription(item: CoursePurchaseOrderItem) {
+	const chapterName = normalizeOptionalText(item.chapterName)
+	if (chapterName) {
+		return chapterName
+	}
+	return normalizeOptionalText(item.courseTitle) || normalizeOptionalText(item.productName) || '-'
+}
+
+/**
+ * 将金额统一格式化为两位小数。
+ */
+function formatPrice(value: number | string | null | undefined) {
+	const amount = Number(value || 0)
+	if (!Number.isFinite(amount)) {
+		return '0.00'
+	}
+	return amount.toFixed(2)
+}
+
+/**
+ * 将接口时间格式化为页面所需的展示文案。
+ */
+function formatDateTime(value?: string) {
+	const normalized = normalizeOptionalText(value)
+	if (!normalized) {
+		return '-'
+	}
+	return normalized.slice(0, 16).replace(/-/g, '.').replace('T', ' ')
+}
+
+/**
+ * 归一化可选数字字段。
+ */
+function normalizeOptionalNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return null
+	}
+	return numberValue
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
 </script>
 
 <style lang="scss" scoped>
-	page {
-		background-color: #FFFFFF;
-	}
+page {
+	background-color: #FFFFFF;
+}
 
-	.container {
-		width: 750rpx;
-		height: 100vh; /* Changed from min-height to height for flex child scroll-view to work */
-		background-color: #FFFFFF;
-		display: flex;
-		flex-direction: column;
-	}
+.container {
+	width: 750rpx;
+	height: 100vh;
+	background-color: #FFFFFF;
+	display: flex;
+	flex-direction: column;
+}
 
-	/* 自定义导航栏 */
-	.custom-navbar {
-		width: 100%;
-		background-color: #FFFFFF;
-		position: relative;
-		z-index: 100;
-	}
+.custom-navbar {
+	width: 100%;
+	background-color: #FFFFFF;
+	position: relative;
+	z-index: 100;
+}
 
-	.nav-content {
-		height: 88rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 32rpx;
-	}
+.nav-content {
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 32rpx;
+}
 
-	.back-icon {
-		width: 40rpx;
-		height: 40rpx;
-	}
+.back-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
 
-	.nav-title {
-		font-size: 36rpx;
-		color: #333333;
-		font-weight: bold;
-	}
+.nav-title {
+	font-size: 36rpx;
+	color: #333333;
+	font-weight: bold;
+}
 
-	.nav-right-placeholder {
-		width: 40rpx;
-	}
+.nav-right-placeholder {
+	width: 40rpx;
+}
 
-	/* 搜索框 */
-	.search-box {
-		width: 686rpx;
-		height: 72rpx;
-		margin: 24rpx 32rpx;
-		background: #F5F5F5;
-		border-radius: 8rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 6rpx 0 24rpx;
-		box-sizing: border-box;
-	}
+.search-box {
+	width: 686rpx;
+	height: 72rpx;
+	margin: 24rpx 32rpx;
+	background: #F5F5F5;
+	border-radius: 8rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 6rpx 0 24rpx;
+	box-sizing: border-box;
+}
 
-	.search-left {
-		display: flex;
-		align-items: center;
-		flex: 1;
+.search-left {
+	display: flex;
+	align-items: center;
+	flex: 1;
+}
 
-		.search-input {
-			margin-left: 12rpx;
-			font-size: 28rpx;
-			color: #333;
-			flex: 1;
-		}
+.search-input {
+	margin-left: 12rpx;
+	font-size: 28rpx;
+	color: #333;
+	flex: 1;
+}
 
-		.ph-color {
-			color: #999999;
-		}
-	}
+.ph-color {
+	color: #999999;
+}
 
-	.search-btn {
-		width: 120rpx;
-		height: 60rpx;
-		background: #C63520;
-		border-radius: 8rpx;
-		color: #FFFFFF;
-		font-size: 28rpx;
-		display: flex;
-		align-items: center;
-		justify-content: center;
-	}
+.search-btn {
+	width: 120rpx;
+	height: 60rpx;
+	background: #C63520;
+	border-radius: 8rpx;
+	color: #FFFFFF;
+	font-size: 28rpx;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+}
 
-	/* 记录列表 */
-	.record-list {
-		flex: 1;
-		height: 0; /* Important for scroll-view inside flex container */
-		padding: 0 32rpx;
-		box-sizing: border-box;
-	}
+.record-list {
+	flex: 1;
+	height: 0;
+	padding: 0 32rpx;
+	box-sizing: border-box;
+}
 
-	.record-item {
-		width: 686rpx;
-		height: 200rpx;
-		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
-		border-radius: 16rpx;
-		margin-bottom: 20rpx;
-		padding: 24rpx;
-		box-sizing: border-box;
-		display: flex;
-		align-items: center;
-		border: 1rpx solid rgba(249, 246, 239, 0.5);
-	}
+.empty-box {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding-top: 160rpx;
+}
 
-	.course-img {
-		width: 200rpx;
-		height: 152rpx;
-		border-radius: 12rpx;
-		margin-right: 24rpx;
-		flex-shrink: 0;
-	}
+.empty-text {
+	font-size: 28rpx;
+	color: #999999;
+}
 
-	.item-info {
-		flex: 1;
-		height: 152rpx;
-		display: flex;
-		flex-direction: column;
-		justify-content: space-between;
-		min-width: 0; /* 配合 flex: 1 解决文本溢出 */
-	}
+.record-item {
+	width: 686rpx;
+	height: 200rpx;
+	background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+	border-radius: 16rpx;
+	margin-bottom: 20rpx;
+	padding: 24rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	border: 1rpx solid rgba(249, 246, 239, 0.5);
+}
 
-	.title-row {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-	}
+.course-img {
+	width: 200rpx;
+	height: 152rpx;
+	border-radius: 12rpx;
+	margin-right: 24rpx;
+	flex-shrink: 0;
+}
 
-	.item-title {
-		font-size: 32rpx;
-		color: #333333;
-		font-weight: bold;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-		flex: 1;
-	}
+.item-info {
+	flex: 1;
+	height: 152rpx;
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+	min-width: 0;
+}
 
-	.refund-tag {
-		padding: 2rpx 12rpx;
-		background-color: rgba(211, 46, 41, 0.1);
-		color: #D32E29;
-		font-size: 20rpx;
-		border-radius: 4rpx;
-		margin-left: 16rpx;
-		flex-shrink: 0;
-	}
+.title-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
 
-	.item-desc {
-		font-size: 24rpx;
-		color: #999999;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-		min-height: 34rpx; /* 保持占位 */
-	}
+.item-title {
+	font-size: 32rpx;
+	color: #333333;
+	font-weight: bold;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	flex: 1;
+}
 
-	.item-bottom {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-	}
+.refund-tag {
+	padding: 2rpx 12rpx;
+	background-color: rgba(211, 46, 41, 0.1);
+	color: #D32E29;
+	font-size: 20rpx;
+	border-radius: 4rpx;
+	margin-left: 16rpx;
+	flex-shrink: 0;
+}
 
-	.item-time {
-		font-size: 24rpx;
-		color: #999999;
-	}
+.item-desc {
+	font-size: 24rpx;
+	color: #999999;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	min-height: 34rpx;
+}
 
-	.price-box {
-		display: flex;
-		align-items: baseline;
-		color: #D32E29;
-	}
+.item-bottom {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
 
-	.price-symbol {
-		font-size: 28rpx;
-		font-weight: bold;
-	}
+.item-time {
+	font-size: 24rpx;
+	color: #999999;
+}
 
-	.price-num {
-		font-size: 36rpx;
-		font-weight: bold;
-	}
+.price-box {
+	display: flex;
+	align-items: baseline;
+	color: #D32E29;
+}
+
+.price-symbol {
+	font-size: 28rpx;
+	font-weight: bold;
+}
+
+.price-num {
+	font-size: 36rpx;
+	font-weight: bold;
+}
 </style>
diff --git a/traditionalApp/pages/courseRecord/service.js b/traditionalApp/pages/courseRecord/service.js
new file mode 100644
index 0000000..6459513
--- /dev/null
+++ b/traditionalApp/pages/courseRecord/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取当前登录用户的购课记录列表。
+ */
+export const getCoursePurchaseOrders = (data) => {
+	return request.get('/api/app/course-purchase-orders', data, false, false)
+}
diff --git a/traditionalApp/pages/courseRecordDetail/courseRecordDetail.vue b/traditionalApp/pages/courseRecordDetail/courseRecordDetail.vue
index 652cd20..6c40edc 100644
--- a/traditionalApp/pages/courseRecordDetail/courseRecordDetail.vue
+++ b/traditionalApp/pages/courseRecordDetail/courseRecordDetail.vue
@@ -1,69 +1,65 @@
 <template>
 	<view class="container">
-		<!-- 顶部背景图 -->
 		<view class="top-bg">
 			<image src="https://mxcd.zhinenganguan.com/file/2026-03-30/5d3dd485-5af1-4913-ba22-c27f668c4660.png" mode="widthFix" class="bg-img"></image>
 		</view>
 
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
-				<!-- 没有标题 -->
 				<view class="nav-right-placeholder"></view>
 			</view>
 		</view>
 
 		<view class="content">
-			<!-- 购买课程模块 -->
 			<view class="section">
 				<text class="section-title">购买课程</text>
 				<view class="course-card">
-					<image class="course-img" src="/static/image/home/class.png" mode="aspectFill"></image>
+					<image class="course-img" :src="resolveCoverImage(detail.coverImage)" mode="aspectFill"></image>
 					<view class="course-info">
-						<view class="title-row">
-							<text class="course-title">课程课程名称标题</text>
+						<view v-if="detail.showRefundTag" class="title-row">
+							<text class="course-title">{{ detail.productName }}</text>
 							<view class="refund-tag">退款</view>
 						</view>
+						<text v-else class="course-title">{{ detail.productName }}</text>
 						<view class="price-box">
 							<text class="price-symbol">¥</text>
-							<text class="price-num">8888.25</text>
+							<text class="price-num">{{ formatPrice(detail.orderAmount) }}</text>
 						</view>
 					</view>
 				</view>
 			</view>
 
-			<!-- 订单信息模块 -->
 			<view class="section mt-32">
 				<text class="section-title">订单信息</text>
 				<view class="order-info-card">
 					<view class="info-item">
 						<text class="info-label">订单编号</text>
-						<text class="info-value">4534654265467</text>
+						<text class="info-value">{{ detail.orderNo || '-' }}</text>
 					</view>
 					<view class="info-item">
 						<text class="info-label">下单时间</text>
-						<text class="info-value">2025-09-09 11:09:09</text>
+						<text class="info-value">{{ formatDateTime(detail.createdAt) }}</text>
 					</view>
 					<view class="info-item">
 						<text class="info-label">实付费用</text>
-						<text class="info-value price-text">¥88.00</text>
+						<text class="info-value price-text">¥{{ formatPrice(detail.orderAmount) }}</text>
 					</view>
 					<view class="info-item">
 						<text class="info-label">支付时间</text>
-						<text class="info-value">2025-09-09 11:09:09</text>
+						<text class="info-value">{{ formatDateTime(detail.paidAt) }}</text>
 					</view>
 					<view class="info-item">
 						<text class="info-label">支付方式</text>
-						<text class="info-value">微信支付</text>
+						<text class="info-value">{{ detail.paymentMethodText || '-' }}</text>
 					</view>
-					<view class="info-item">
+					<view v-if="detail.showRefundTag" class="info-item">
 						<text class="info-label">退款金额</text>
-						<text class="info-value refund-text">¥88.00</text>
+						<text class="info-value refund-text">¥{{ formatPrice(detail.refundAmount) }}</text>
 					</view>
-					<view class="info-item">
+					<view v-if="detail.showRefundTag" class="info-item">
 						<text class="info-label">退款时间</text>
-						<text class="info-value">2025-09-09 11:09:09</text>
+						<text class="info-value">{{ formatDateTime(detail.refundedAt) }}</text>
 					</view>
 				</view>
 			</view>
@@ -72,191 +68,351 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import { getCoursePurchaseOrderDetail } from './service'
 
-	const statusBarHeight = ref(20);
+interface CoursePurchaseOrderDetail {
+	id?: number | null
+	orderNo?: string
+	courseId?: number | null
+	courseVideoId?: number | null
+	productName?: string
+	courseTitle?: string
+	chapterName?: string
+	coverImage?: string
+	orderAmount?: number | string | null
+	paymentMethod?: string
+	paymentMethodText?: string
+	status?: string
+	refundAmount?: number | string | null
+	showRefundTag?: boolean
+	createdAt?: string
+	paidAt?: string
+	refundedAt?: string
+}
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
 
-	const goBack = () => {
-		uni.navigateBack();
-	};
+const statusBarHeight = ref(20)
+const detail = ref<CoursePurchaseOrderDetail>(createDefaultDetail())
+
+/**
+ * 创建购课记录详情默认值,避免页面首屏空引用。
+ */
+function createDefaultDetail(): CoursePurchaseOrderDetail {
+	return {
+		id: null,
+		orderNo: '',
+		courseId: null,
+		courseVideoId: null,
+		productName: '课程名称',
+		courseTitle: '',
+		chapterName: '',
+		coverImage: '',
+		orderAmount: null,
+		paymentMethod: '',
+		paymentMethodText: '',
+		status: '',
+		refundAmount: null,
+		showRefundTag: false,
+		createdAt: '',
+		paidAt: '',
+		refundedAt: '',
+	}
+}
+
+/**
+ * 页面加载时初始化状态栏高度并根据订单编号拉取详情。
+ */
+onLoad((options) => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+	loadCoursePurchaseOrderDetail(options?.orderNo)
+})
+
+/**
+ * 拉取当前登录用户的购课记录详情。
+ */
+async function loadCoursePurchaseOrderDetail(orderNo?: string) {
+	const normalizedOrderNo = normalizeOptionalText(orderNo)
+	if (!normalizedOrderNo) {
+		return
+	}
+	try {
+		const response = await getCoursePurchaseOrderDetail(normalizedOrderNo)
+		if (response?.code !== 0 || !response.data) {
+			return
+		}
+		detail.value = normalizeDetail(response.data)
+	} catch {
+		// 请求层会统一处理未登录跳转和异常提示,这里不重复 toast。
+	}
+}
+
+/**
+ * 将接口返回的详情归一化为页面渲染结构。
+ */
+function normalizeDetail(value: unknown): CoursePurchaseOrderDetail {
+	const record = (value && typeof value === 'object' ? value : {}) as Partial<CoursePurchaseOrderDetail>
+	return {
+		id: normalizeOptionalNumber(record.id),
+		orderNo: normalizeOptionalText(record.orderNo),
+		courseId: normalizeOptionalNumber(record.courseId),
+		courseVideoId: normalizeOptionalNumber(record.courseVideoId),
+		productName: normalizeOptionalText(record.productName) || '课程名称',
+		courseTitle: normalizeOptionalText(record.courseTitle),
+		chapterName: normalizeOptionalText(record.chapterName),
+		coverImage: normalizeOptionalText(record.coverImage),
+		orderAmount: record.orderAmount ?? null,
+		paymentMethod: normalizeOptionalText(record.paymentMethod),
+		paymentMethodText: normalizeOptionalText(record.paymentMethodText),
+		status: normalizeOptionalText(record.status),
+		refundAmount: record.refundAmount ?? null,
+		showRefundTag: !!record.showRefundTag,
+		createdAt: normalizeOptionalText(record.createdAt),
+		paidAt: normalizeOptionalText(record.paidAt),
+		refundedAt: normalizeOptionalText(record.refundedAt),
+	}
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 解析课程封面图,避免接口为空时页面出现空白。
+ */
+function resolveCoverImage(value?: string) {
+	return normalizeOptionalText(value) || DEFAULT_COURSE_IMAGE
+}
+
+/**
+ * 将金额统一格式化为两位小数。
+ */
+function formatPrice(value: number | string | null | undefined) {
+	const amount = Number(value || 0)
+	if (!Number.isFinite(amount)) {
+		return '0.00'
+	}
+	return amount.toFixed(2)
+}
+
+/**
+ * 将接口时间格式化为页面所需的展示文案。
+ */
+function formatDateTime(value?: string) {
+	const normalized = normalizeOptionalText(value)
+	if (!normalized) {
+		return '-'
+	}
+	return normalized.slice(0, 19).replace('T', ' ')
+}
+
+/**
+ * 归一化可选数字字段。
+ */
+function normalizeOptionalNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return null
+	}
+	return numberValue
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
 </script>
 
 <style lang="scss" scoped>
-	page {
-		background-color: #F8F6F4;
-	}
+page {
+	background-color: #F8F6F4;
+}
 
-	.container {
-		width: 750rpx;
-		min-height: 100vh;
-		position: relative;
-		background-color: #F8F6F4;
-	}
+.container {
+	width: 750rpx;
+	min-height: 100vh;
+	position: relative;
+	background-color: #F8F6F4;
+}
 
-	.top-bg {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 750rpx;
-		z-index: 0;
+.top-bg {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 750rpx;
+	z-index: 0;
+}
 
-		.bg-img {
-			width: 750rpx;
-			height: auto;
-		}
-	}
+.bg-img {
+	width: 750rpx;
+	height: auto;
+}
 
-	/* 自定义导航栏 */
-	.custom-navbar {
-		width: 100%;
-		position: relative;
-		z-index: 100;
-	}
+.custom-navbar {
+	width: 100%;
+	position: relative;
+	z-index: 100;
+}
 
-	.nav-content {
-		height: 88rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 32rpx;
-	}
+.nav-content {
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 32rpx;
+}
 
-	.back-icon {
-		width: 40rpx;
-		height: 40rpx;
-	}
+.back-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
 
-	.nav-right-placeholder {
-		width: 40rpx;
-	}
+.nav-right-placeholder {
+	width: 40rpx;
+}
 
-	.content {
-		position: relative;
-		z-index: 10;
-		padding: 24rpx 32rpx;
-	}
+.content {
+	position: relative;
+	z-index: 10;
+	padding: 24rpx 32rpx;
+}
 
-	.section {
-		margin-bottom: 40rpx;
-	}
+.section {
+	margin-bottom: 40rpx;
+}
 
-	.mt-32 {
-		margin-top: 32rpx;
-	}
+.mt-32 {
+	margin-top: 32rpx;
+}
 
-	.section-title {
-		font-size: 32rpx;
-		color: #1A1A1A;
-		font-weight: bold;
-		margin-bottom: 24rpx;
-		display: block;
-	}
+.section-title {
+	font-size: 32rpx;
+	color: #1A1A1A;
+	font-weight: bold;
+	margin-bottom: 24rpx;
+	display: block;
+}
 
-	/* 课程卡片 */
-	.course-card {
-		width: 100%;
-		height: 160rpx;
-		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
-		border-radius: 16rpx;
-		padding: 24rpx;
-		box-sizing: border-box;
-		display: flex;
-		align-items: center;
-	}
+.course-card {
+	width: 100%;
+	height: 160rpx;
+	background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+	border-radius: 16rpx;
+	padding: 24rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+}
 
-	.course-img {
-		width: 152rpx;
-		height: 112rpx;
-		border-radius: 12rpx;
-		margin-right: 24rpx;
-		flex-shrink: 0;
-	}
+.course-img {
+	width: 152rpx;
+	height: 112rpx;
+	border-radius: 12rpx;
+	margin-right: 24rpx;
+	flex-shrink: 0;
+}
 
-	.course-info {
-		flex: 1;
-		height: 112rpx;
-		display: flex;
-		flex-direction: column;
-		justify-content: space-between;
-	}
+.course-info {
+	flex: 1;
+	height: 112rpx;
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+}
 
-	.title-row {
-		display: flex;
-		justify-content: space-between;
-		align-items: flex-start;
-	}
+.title-row {
+	display: flex;
+	justify-content: space-between;
+	align-items: flex-start;
+}
 
-	.course-title {
-		font-size: 32rpx;
-		color: #333333;
-		font-weight: bold;
-	}
+.course-title {
+	font-size: 32rpx;
+	color: #333333;
+	font-weight: bold;
+	flex: 1;
+	min-width: 0;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.refund-tag {
-		padding: 2rpx 12rpx;
-		background-color: rgba(211, 46, 41, 0.1);
-		color: #D32E29;
-		font-size: 20rpx;
-		border-radius: 4rpx;
-	}
+.refund-tag {
+	padding: 2rpx 12rpx;
+	background-color: rgba(211, 46, 41, 0.1);
+	color: #D32E29;
+	font-size: 20rpx;
+	border-radius: 4rpx;
+	margin-left: 16rpx;
+	flex-shrink: 0;
+}
 
-	.price-box {
-		display: flex;
-		align-items: baseline;
-		justify-content: flex-end;
-		color: #D32E29;
-	}
+.price-box {
+	display: flex;
+	align-items: baseline;
+	justify-content: flex-end;
+	color: #D32E29;
+}
 
-	.price-symbol {
-		font-size: 24rpx;
-		font-weight: bold;
-	}
+.price-symbol {
+	font-size: 24rpx;
+	font-weight: bold;
+}
 
-	.price-num {
-		font-size: 32rpx;
-		font-weight: bold;
-	}
+.price-num {
+	font-size: 32rpx;
+	font-weight: bold;
+}
 
-	/* 订单信息卡片 */
-	.order-info-card {
-		background-color: #FFFFFF;
-		border-radius: 16rpx;
-		padding: 32rpx;
-	}
+.order-info-card {
+	background-color: #FFFFFF;
+	border-radius: 16rpx;
+	padding: 32rpx;
+}
 
-	.info-item {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		margin-bottom: 32rpx;
+.info-item {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	margin-bottom: 32rpx;
+}
 
-		&:last-child {
-			margin-bottom: 0;
-		}
-	}
+.info-item:last-child {
+	margin-bottom: 0;
+}
 
-	.info-label {
-		font-size: 28rpx;
-		color: #86909C;
-	}
+.info-label {
+	font-size: 28rpx;
+	color: #86909C;
+}
 
-	.info-value {
-		font-size: 28rpx;
-		color: #1A1A1A;
-		font-weight: 500;
-	}
+.info-value {
+	font-size: 28rpx;
+	color: #1A1A1A;
+	font-weight: 500;
+	text-align: right;
+	margin-left: 24rpx;
+	flex: 1;
+	word-break: break-all;
+}
 
-	.price-text {
-		font-weight: bold;
-	}
+.price-text {
+	font-weight: bold;
+}
 
-	.refund-text {
-		color: #D32E29;
-		font-weight: bold;
-	}
+.refund-text {
+	color: #D32E29;
+	font-weight: bold;
+}
 </style>
diff --git a/traditionalApp/pages/courseRecordDetail/service.js b/traditionalApp/pages/courseRecordDetail/service.js
new file mode 100644
index 0000000..fcb8a62
--- /dev/null
+++ b/traditionalApp/pages/courseRecordDetail/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取当前登录用户的购课记录详情。
+ */
+export const getCoursePurchaseOrderDetail = (orderNo) => {
+	return request.get(`/api/app/course-purchase-orders/${encodeURIComponent(orderNo)}`, {}, false, false)
+}
diff --git a/traditionalApp/pages/downloads/downloads.vue b/traditionalApp/pages/downloads/downloads.vue
index 63d218e..47e3870 100644
--- a/traditionalApp/pages/downloads/downloads.vue
+++ b/traditionalApp/pages/downloads/downloads.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -10,26 +9,27 @@
 		</view>
 
 		<scroll-view scroll-y class="downloads-list">
+			<view v-if="downloadList.length === 0" class="empty-box">
+				<text class="empty-text">暂无下载记录</text>
+			</view>
 			<block v-for="(item, index) in downloadList" :key="item.id">
 				<view class="list-item-wrapper" @tap="handleItemClick(index)">
-					<!-- 批量删除模式下的左侧复选框 -->
 					<view class="checkbox-col" v-if="isBatchMode">
-						<image class="check-icon"
+						<image
+							class="check-icon"
 							:src="item.selected ? '/static/image/course/check.png' : '/static/image/course/uncheck.png'"
-							mode="aspectFit"></image>
+							mode="aspectFit"
+						></image>
 					</view>
 
 					<view class="swipe-wrap" :class="{ 'in-batch-mode': isBatchMode }">
-						<u-swipe-action :options="swipeOptions" :index="index" @click="handleDelete(index)"
-							:disabled="isBatchMode">
+						<u-swipe-action :options="swipeOptions" :index="index" @click="handleDelete(index)" :disabled="isBatchMode">
 							<view class="course-item-container">
-								<view class="course-item"> 
-									<!-- 图片 -->
+								<view class="course-item">
 									<view class="img-wrap">
 										<image class="course-img" :src="item.image" mode="aspectFill"></image>
 									</view>
 
-									<!-- 课程信息 -->
 									<view class="course-info">
 										<view class="title-wrap">
 											<text class="course-title">{{ item.title }}</text>
@@ -41,8 +41,7 @@
 										</view>
 									</view>
 								</view>
-								
-								<!-- 下载进度条 -->
+
 								<view class="progress-row" v-if="item.status === 'downloading'">
 									<text class="progress-label">下载中</text>
 									<view class="progress-bar-wrap">
@@ -60,372 +59,664 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
+import { getHealthCourseDownloads, deleteHealthCourseDownloads } from './service'
+import {
+	getDownloadTaskChangeEventName,
+	listDownloadTasks,
+	removeDownloadTasks,
+} from '@/utils/download-task-storage.js'
 
-	const statusBarHeight = ref(20);
-	const isBatchMode = ref(false);
+interface RemoteDownloadRecord {
+	id?: number | null
+	courseId?: number | null
+	courseVideoId?: number | null
+	title?: string
+	subtitle?: string
+	coverImage?: string
+	durationSeconds?: number | null
+	durationText?: string
+	videoUrl?: string
+	downloadedAt?: string
+}
 
-	// 模拟数据
-	const downloadList = ref([
-		{
-			id: 1,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloaded', // downloaded, downloading
-			progress: 100,
-			selected: false
+interface LocalDownloadTask {
+	clientTaskId?: string
+	courseId?: number | null
+	courseVideoId?: number | null
+	title?: string
+	subtitle?: string
+	coverImage?: string
+	durationSeconds?: number | null
+	durationText?: string
+	videoUrl?: string
+	progress?: number | null
+	updatedAt?: string
+}
+
+type DownloadSourceType = 'remoteCompleted' | 'localDownloading'
+
+interface DownloadListItem {
+	id: string
+	remoteId: number | null
+	clientTaskId: string
+	courseId: number | null
+	courseVideoId: number | null
+	title: string
+	subtitle: string
+	duration: string
+	image: string
+	status: 'downloaded' | 'downloading'
+	progress: number
+	selected: boolean
+	sourceType: DownloadSourceType
+	sortTime: number
+}
+
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
+
+const statusBarHeight = ref(20)
+const isBatchMode = ref(false)
+const downloadList = ref<DownloadListItem[]>([])
+
+const swipeOptions = ref([
+	{
+		text: '删除',
+		style: {
+			backgroundColor: '#F53F3F',
+			color: '#fff',
+			fontSize: '28rpx',
+			width: '120rpx',
+			height: '120rpx',
+			borderRadius: '50%',
+			display: 'flex',
+			justifyContent: 'center',
+			alignItems: 'center',
+			margin: '0 30rpx',
 		},
-		{
-			id: 2,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloaded',
-			progress: 100,
-			selected: false
-		},
-		{
-			id: 3,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloaded',
-			progress: 100,
-			selected: false
-		},
-		{
-			id: 4,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloaded',
-			progress: 100,
-			selected: false
-		},
-		{
-			id: 5,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloading',
-			progress: 40,
-			selected: false
-		},
-		{
-			id: 6,
-			title: '方剂学方剂学',
-			subtitle: '中医药剂学',
-			duration: '1:15:08',
-			image: '/static/image/home/doctor.png',
-			status: 'downloading',
-			progress: 40,
-			selected: false
+	},
+])
+
+/**
+ * 页面加载时初始化状态栏高度并注册下载任务刷新监听。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+	uni.$on(getDownloadTaskChangeEventName(), handleDownloadTaskChanged)
+})
+
+/**
+ * 页面展示时重新拉取远端完成记录,并合并本地下载中任务。
+ */
+onShow(() => {
+	loadDownloadList()
+})
+
+/**
+ * 页面销毁时解绑下载任务监听,避免重复回调。
+ */
+onUnload(() => {
+	uni.$off(getDownloadTaskChangeEventName(), handleDownloadTaskChanged)
+})
+
+/**
+ * 拉取当前登录用户的下载列表,并与本地下载中的临时任务合并。
+ */
+async function loadDownloadList() {
+	const localItems = normalizeLocalDownloadItems(listDownloadTasks(resolveCurrentUserId()))
+	try {
+		const response = await getHealthCourseDownloads()
+		if (response?.code !== 0) {
+			downloadList.value = sortDownloadList(localItems)
+			return
 		}
-	]);
+		const remoteItems = normalizeRemoteDownloadItems(response.data)
+		downloadList.value = sortDownloadList([...localItems, ...remoteItems])
+	} catch {
+		downloadList.value = sortDownloadList(localItems)
+	}
+}
 
-	// u-swipe-action 右侧滑出选项
-	const swipeOptions = ref([
-		{
-			text: '删除',
-			style: {
-				backgroundColor: '#F53F3F',
-				color: '#fff',
-				fontSize: '28rpx',
-				width: '120rpx',
-				height: '120rpx',
-				borderRadius: '50%',
-				display: 'flex',
-				justifyContent: 'center',
-				alignItems: 'center',
-				margin: '0 30rpx'
+/**
+ * 下载任务发生变化时刷新页面列表,保证下载中进度实时回显。
+ */
+function handleDownloadTaskChanged() {
+	loadDownloadList()
+}
+
+/**
+ * 返回上一页;如果没有历史栈,则回到个人中心。
+ */
+function goBack() {
+	if (getCurrentPages().length > 1) {
+		uni.navigateBack()
+		return
+	}
+	uni.switchTab({
+		url: '/pages/mine/mine',
+	})
+}
+
+/**
+ * 切换批量模式;完成时执行批量删除。
+ */
+function toggleBatchEdit() {
+	if (!isBatchMode.value) {
+		isBatchMode.value = true
+		return
+	}
+	handleBatchDelete()
+}
+
+/**
+ * 批量模式下切换勾选;普通模式下当前不扩展新交互,保持现状不跳转。
+ */
+function handleItemClick(index: number) {
+	const currentItem = downloadList.value[index]
+	if (!currentItem) {
+		return
+	}
+	if (isBatchMode.value) {
+		currentItem.selected = !currentItem.selected
+		return
+	}
+}
+
+/**
+ * 左滑删除当前下载项。
+ */
+function handleDelete(index: number) {
+	const currentItem = downloadList.value[index]
+	if (!currentItem) {
+		return
+	}
+	uni.showModal({
+		title: '提示',
+		content: '确认删除该下载记录?',
+		success: async (res) => {
+			if (!res.confirm) {
+				return
+			}
+			await deleteDownloadItems([currentItem])
+		},
+	})
+}
+
+/**
+ * 批量删除当前勾选项;未勾选时直接退出批量模式。
+ */
+async function handleBatchDelete() {
+	const selectedItems = downloadList.value.filter((item) => item.selected)
+	if (selectedItems.length === 0) {
+		resetBatchMode()
+		return
+	}
+	await deleteDownloadItems(selectedItems)
+}
+
+/**
+ * 删除勾选或单条下载项,并分别处理远端完成记录与本地下载中任务。
+ */
+async function deleteDownloadItems(items: DownloadListItem[]) {
+	const remoteIds = items
+		.filter((item) => item.sourceType === 'remoteCompleted' && item.remoteId)
+		.map((item) => Number(item.remoteId))
+	const localTaskIds = items
+		.filter((item) => item.sourceType === 'localDownloading' && item.clientTaskId)
+		.map((item) => item.clientTaskId)
+
+	try {
+		if (remoteIds.length > 0) {
+			const response = await deleteHealthCourseDownloads(remoteIds)
+			if (response?.code !== 0) {
+				return
 			}
 		}
-	]);
-
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
-
-	const goBack = () => {
-		uni.navigateBack();
-	};
-
-	const toggleBatchEdit = () => {
-		isBatchMode.value = !isBatchMode.value;
-		if (!isBatchMode.value) {
-			// 如果取消批量操作,清空所有选中状态
-			downloadList.value.forEach(item => {
-				item.selected = false;
-			});
+		if (localTaskIds.length > 0) {
+			// 下载中删除只移除本地任务展示,不强制终止底层原生下载。
+			removeDownloadTasks(resolveCurrentUserId(), localTaskIds)
 		}
-	};
+		downloadList.value = downloadList.value.filter((item) => !items.some((target) => target.id === item.id))
+		resetBatchMode()
+		uni.showToast({
+			title: '已删除',
+			icon: 'none',
+		})
+	} catch {
+		// 请求层会统一处理异常提示,这里不重复 toast。
+	}
+}
 
-	const handleItemClick = (index : number) => {
-		if (isBatchMode.value) {
-			downloadList.value[index].selected = !downloadList.value[index].selected;
-		} else {
-			// 播放视频逻辑
-			console.log('Play video');
+/**
+ * 退出批量模式并清理勾选状态。
+ */
+function resetBatchMode() {
+	isBatchMode.value = false
+	downloadList.value.forEach((item) => {
+		item.selected = false
+	})
+}
+
+/**
+ * 将后端返回的已完成下载记录归一化为页面项。
+ */
+function normalizeRemoteDownloadItems(value: unknown): DownloadListItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const record = (item && typeof item === 'object' ? item : {}) as RemoteDownloadRecord
+		const remoteId = normalizeOptionalNumber(record.id)
+		return {
+			id: `remote-${remoteId || Math.random()}`,
+			remoteId,
+			clientTaskId: '',
+			courseId: normalizeOptionalNumber(record.courseId),
+			courseVideoId: normalizeOptionalNumber(record.courseVideoId),
+			title: normalizeOptionalText(record.title) || '课程章节',
+			subtitle: normalizeOptionalText(record.subtitle) || '养生课程',
+			duration: resolveDurationText(record.durationText, record.durationSeconds),
+			image: resolveCourseImage(record.coverImage),
+			status: 'downloaded',
+			progress: 100,
+			selected: false,
+			sourceType: 'remoteCompleted',
+			sortTime: resolveDateTime(record.downloadedAt),
 		}
-	};
+	})
+}
 
-	const handleDelete = (index : number) => {
-		uni.showModal({
-			title: '提示',
-			content: '确认删除该下载记录?',
-			success: (res) => {
-				if (res.confirm) {
-					downloadList.value.splice(index, 1);
-					uni.showToast({
-						title: '已删除',
-						icon: 'none'
-					});
-				}
-			}
-		});
-	};
+/**
+ * 将本地下载中任务归一化为页面项。
+ */
+function normalizeLocalDownloadItems(value: LocalDownloadTask[]): DownloadListItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const clientTaskId = normalizeOptionalText(item.clientTaskId) || `local-${Math.random()}`
+		return {
+			id: `local-${clientTaskId}`,
+			remoteId: null,
+			clientTaskId,
+			courseId: normalizeOptionalNumber(item.courseId),
+			courseVideoId: normalizeOptionalNumber(item.courseVideoId),
+			title: normalizeOptionalText(item.title) || '课程章节',
+			subtitle: normalizeOptionalText(item.subtitle) || '养生课程',
+			duration: resolveDurationText(item.durationText, item.durationSeconds),
+			image: resolveCourseImage(item.coverImage),
+			status: 'downloading',
+			progress: normalizeProgress(item.progress),
+			selected: false,
+			sourceType: 'localDownloading',
+			sortTime: resolveDateTime(item.updatedAt),
+		}
+	})
+}
+
+/**
+ * 按“下载中优先、时间倒序”排序下载列表。
+ */
+function sortDownloadList(items: DownloadListItem[]) {
+	return [...items].sort((left, right) => {
+		if (left.status !== right.status) {
+			return left.status === 'downloading' ? -1 : 1
+		}
+		return right.sortTime - left.sortTime
+	})
+}
+
+/**
+ * 读取当前缓存中的 APP 用户标识。
+ */
+function resolveCurrentUserId() {
+	const cachedUser = uni.getStorageSync('appUserInfo')
+	if (!cachedUser || typeof cachedUser !== 'object') {
+		return 0
+	}
+	const userId = Number((cachedUser as { id?: number | string }).id || 0)
+	if (!Number.isFinite(userId) || userId <= 0) {
+		return 0
+	}
+	return userId
+}
+
+/**
+ * 解析课程封面图。
+ */
+function resolveCourseImage(value?: string) {
+	return normalizeOptionalText(value) || DEFAULT_COURSE_IMAGE
+}
+
+/**
+ * 优先使用接口返回的时长文案,没有时再用秒数自行兜底格式化。
+ */
+function resolveDurationText(durationText?: string, durationSeconds?: number | null) {
+	const normalizedText = normalizeOptionalText(durationText)
+	if (normalizedText) {
+		return normalizedText
+	}
+	return formatDurationSeconds(durationSeconds)
+}
+
+/**
+ * 将秒数格式化为页面所需时长文案。
+ */
+function formatDurationSeconds(value?: number | null) {
+	const totalSeconds = Math.max(0, Number(value || 0))
+	if (!Number.isFinite(totalSeconds)) {
+		return '00:00'
+	}
+	const hours = Math.floor(totalSeconds / 3600)
+	const minutes = Math.floor((totalSeconds % 3600) / 60)
+	const seconds = Math.floor(totalSeconds % 60)
+	if (hours > 0) {
+		return `${padNumber(hours)}:${padNumber(minutes)}:${padNumber(seconds)}`
+	}
+	return `${padNumber(minutes)}:${padNumber(seconds)}`
+}
+
+/**
+ * 补齐两位数字展示。
+ */
+function padNumber(value: number) {
+	return String(value).padStart(2, '0')
+}
+
+/**
+ * 将可选数字归一化为空值或有效数字。
+ */
+function normalizeOptionalNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return null
+	}
+	return numberValue
+}
+
+/**
+ * 归一化可选文本。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
+
+/**
+ * 将时间字符串解析为排序所需的时间戳。
+ */
+function resolveDateTime(value?: string) {
+	const normalizedText = normalizeOptionalText(value)
+	if (!normalizedText) {
+		return 0
+	}
+	const timestamp = new Date(normalizedText.replace(' ', 'T')).getTime()
+	if (!Number.isFinite(timestamp)) {
+		return 0
+	}
+	return timestamp
+}
+
+/**
+ * 归一化进度值,保证范围在 0 到 100 之间。
+ */
+function normalizeProgress(value: unknown) {
+	const progressValue = Math.round(Number(value || 0))
+	if (!Number.isFinite(progressValue)) {
+		return 0
+	}
+	if (progressValue < 0) {
+		return 0
+	}
+	if (progressValue > 100) {
+		return 100
+	}
+	return progressValue
+}
 </script>
 
 <style lang="scss" scoped>
-	page {
-		background-color: #ffffff;
-	}
+page {
+	background-color: #ffffff;
+}
 
-	.container {
-		width: 100vw;
-		height: 100vh;
-		background-color: #ffffff;
-		display: flex;
-		flex-direction: column;
-	}
+.container {
+	width: 100vw;
+	height: 100vh;
+	background-color: #ffffff;
+	display: flex;
+	flex-direction: column;
+}
 
-	/* 自定义导航栏 */
-	.custom-navbar {
-		width: 100%;
-		background-color: #FFFFFF;
-		position: relative;
-		z-index: 100;
-	}
+.custom-navbar {
+	width: 100%;
+	background-color: #FFFFFF;
+	position: relative;
+	z-index: 100;
+}
 
-	.nav-content {
-		height: 88rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 32rpx;
-	}
+.nav-content {
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 32rpx;
+}
 
-	.back-icon {
+.back-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
+
+.nav-title {
+	font-size: 36rpx;
+	color: #333333;
+	font-weight: bold;
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+}
+
+.nav-right-text {
+	font-size: 28rpx;
+	color: #333333;
+}
+
+.downloads-list {
+	flex: 1;
+	height: 0;
+	padding: 24rpx 32rpx;
+	box-sizing: border-box;
+	width: 100%;
+}
+
+.empty-box {
+	padding-top: 160rpx;
+	display: flex;
+	justify-content: center;
+}
+
+.empty-text {
+	font-size: 28rpx;
+	color: #999999;
+}
+
+.list-item-wrapper {
+	display: flex;
+	align-items: flex-start;
+	width: 100%;
+	margin-bottom: 24rpx;
+}
+
+.checkbox-col {
+	width: 60rpx;
+	display: flex;
+	justify-content: flex-start;
+	align-items: center;
+	flex-shrink: 0;
+	height: 200rpx;
+
+	.check-icon {
 		width: 40rpx;
 		height: 40rpx;
 	}
+}
 
-	.nav-title {
-		font-size: 36rpx;
-		color: #333333;
-		font-weight: bold;
-		position: absolute;
-		left: 50%;
-		transform: translateX(-50%);
-	}
+.swipe-wrap {
+	flex: 1;
+	min-width: 0;
+	transition: all 0.3s;
+}
 
-	.nav-right-text {
-		font-size: 28rpx;
-		color: #333333;
-	}
+:deep(.u-swipe-action) {
+	background-color: transparent !important;
+}
 
-	/* 下载列表 */
-	.downloads-list {
-		flex: 1;
-		height: 0;
-		padding: 24rpx 32rpx;
-		box-sizing: border-box;
-		width: 100%;
-	}
+:deep(.u-swipe-action-item) {
+	background-color: transparent !important;
+}
 
-	.list-item-wrapper {
-		display: flex;
-		align-items: flex-start;
-		width: 100%;
-		margin-bottom: 24rpx;
-	}
+:deep(.u-swipe-del) {
+	background-color: transparent !important;
+	display: flex;
+	align-items: center;
+	height: 100%;
+	width: 192rpx !important;
+	height: 180rpx !important;
+	margin: unset !important;
+}
 
-	.checkbox-col {
-		width: 60rpx;
-		display: flex;
-		justify-content: flex-start;
-		align-items: center;
-		flex-shrink: 0;
-		/* Height set to match the course card to enable vertical centering */
-		height: 200rpx;
-		
-		.check-icon {
-			width: 40rpx;
-			height: 40rpx;
-		}
-	}
+:deep(.u-btn-text) {
+	display: none;
+}
 
-	.swipe-wrap {
-		flex: 1;
-		min-width: 0;
-		transition: all 0.3s;
+:deep(.u-swipe-del::after) {
+	content: '';
+	width: 120rpx;
+	height: 120rpx;
+	background-color: transparent;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin: 0 32rpx;
+	background-image: url('/static/image/course/delete.png');
+	background-size: 120rpx 120rpx;
+	background-repeat: no-repeat;
+	background-position: center;
+}
 
-		&.in-batch-mode {
-			/* You can add specific styles when in batch mode if needed */
-		}
-	}
+.course-item-container {
+	width: 100%;
+	background-color: transparent;
+}
 
-	// 修改 u-swipe-action 的样式适应圆形删除按钮设计
-	:deep(.u-swipe-action) {
-		background-color: transparent !important;
-	}
+.course-item {
+	width: 100%;
+	height: 200rpx;
+	background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+	padding: 24rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	border-radius: 24rpx;
+}
 
-	/* The actual scroll view container inside u-swipe-action */
-	:deep(.u-swipe-action-item) {
-		background-color: transparent !important;
-	}
+.img-wrap {
+	width: 152rpx;
+	height: 152rpx;
+	border-radius: 16rpx;
+	margin-right: 24rpx;
+	flex-shrink: 0;
+	background-color: #BDBDBD;
+}
 
-	:deep(.u-swipe-del) {
-		background-color: transparent !important;
-		display: flex;
-		align-items: center;
-		height: 100%;
-		width: 192rpx !important;
-		height: 180rpx !important;
-		margin: unset !important;
-	}
+.course-img {
+	width: 100%;
+	height: 100%;
+	border-radius: 16rpx;
+}
 
-	:deep(.u-btn-text) {
-		display: none; // 隐藏文字,我们在样式里画个垃圾桶
-	}
+.course-info {
+	flex: 1;
+	height: 152rpx;
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+	min-width: 0;
+}
 
-	:deep(.u-swipe-del::after) {
-		content: '';
-		width: 120rpx;
-		height: 120rpx;
-		background-color: transparent;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		margin: 0 32rpx;
-		background-image: url('/static/image/course/delete.png');
-		background-size: 120rpx 120rpx;
-		background-repeat: no-repeat;
-		background-position: center;
-	}
+.title-wrap {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+}
 
-	.course-item-container {
-		width: 100%;
-		background-color: transparent;
-	}
+.course-title {
+	font-size: 32rpx;
+	color: #1A1A1A;
+	font-weight: bold;
+	flex: 1;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.course-item {
-		width: 100%;
-		height: 200rpx;
-		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
-		padding: 24rpx;
-		box-sizing: border-box;
-		display: flex;
-		align-items: center;
-		border-radius: 24rpx;
-	}
+.course-bottom {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
 
-	.img-wrap {
-		width: 152rpx;
-		height: 152rpx;
-		border-radius: 16rpx;
-		margin-right: 24rpx;
-		flex-shrink: 0;
-		background-color: #BDBDBD; // 灰底
-	}
+.course-subtitle {
+	font-size: 26rpx;
+	color: #86909C;
+	max-width: 420rpx;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.course-img {
-		width: 100%;
-		height: 100%;
-		border-radius: 16rpx;
-	}
+.course-duration {
+	font-size: 26rpx;
+	color: #86909C;
+}
 
-	.course-info {
-		flex: 1;
-		height: 152rpx;
-		display: flex;
-		flex-direction: column;
-		justify-content: space-between;
-		min-width: 0;
-	}
+.progress-row {
+	display: flex;
+	align-items: center;
+	padding: 16rpx 0 0;
+	width: 100%;
+}
 
-	.title-wrap {
-		display: flex;
-		align-items: center;
-		flex-wrap: wrap;
-	}
+.progress-label {
+	font-size: 24rpx;
+	color: #1A1A1A;
+	margin-right: 16rpx;
+}
 
-	.course-title {
-		font-size: 32rpx;
-		color: #1A1A1A;
-		font-weight: bold;
-		flex: 1;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
+.progress-bar-wrap {
+	flex: 1;
+	height: 10rpx;
+	background-color: #E5E6EB;
+	border-radius: 5rpx;
+	overflow: hidden;
+	margin-right: 16rpx;
+}
 
-	.course-bottom {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-	}
+.progress-bar-inner {
+	height: 100%;
+	background-color: #D32E29;
+	border-radius: 5rpx;
+}
 
-	.course-subtitle {
-		font-size: 26rpx;
-		color: #86909C;
-	}
-
-	.course-duration {
-		font-size: 26rpx;
-		color: #86909C;
-	}
-
-	/* 下载进度条 */
-	.progress-row {
-		display: flex;
-		align-items: center;
-		padding: 16rpx 0 0;
-		width: 100%;
-	}
-
-	.progress-label {
-		font-size: 24rpx;
-		color: #1A1A1A;
-		margin-right: 16rpx;
-	}
-
-	.progress-bar-wrap {
-		flex: 1;
-		height: 10rpx;
-		background-color: #E5E6EB;
-		border-radius: 5rpx;
-		overflow: hidden;
-		margin-right: 16rpx;
-	}
-
-	.progress-bar-inner {
-		height: 100%;
-		background-color: #D32E29; /* 红色进度条 */
-		border-radius: 5rpx;
-	}
-
-	.progress-value {
-		font-size: 24rpx;
-		color: #86909C;
-		width: 60rpx;
-		text-align: right;
-	}
+.progress-value {
+	font-size: 24rpx;
+	color: #86909C;
+	width: 60rpx;
+	text-align: right;
+}
 </style>
diff --git a/traditionalApp/pages/downloads/service.js b/traditionalApp/pages/downloads/service.js
new file mode 100644
index 0000000..da32415
--- /dev/null
+++ b/traditionalApp/pages/downloads/service.js
@@ -0,0 +1,15 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取当前登录用户的课程下载记录。
+ */
+export const getHealthCourseDownloads = () => {
+	return request.get('/api/app/health-course-downloads', {}, false, false)
+}
+
+/**
+ * 批量删除当前登录用户的课程下载记录。
+ */
+export const deleteHealthCourseDownloads = (ids) => {
+	return request.delete('/api/app/health-course-downloads', { ids }, false, true)
+}
diff --git a/traditionalApp/pages/favorites/favorites.vue b/traditionalApp/pages/favorites/favorites.vue
index 18eb1ae..2e670e5 100644
--- a/traditionalApp/pages/favorites/favorites.vue
+++ b/traditionalApp/pages/favorites/favorites.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -10,47 +9,50 @@
 		</view>
 
 		<scroll-view scroll-y class="favorites-list">
-			<block v-for="(item, index) in favoriteList" :key="item.id">
+			<view v-if="favoriteList.length === 0" class="empty-box">
+				<text class="empty-text">暂无收藏课程</text>
+			</view>
+			<block v-for="(item, index) in favoriteList" :key="item.courseId || index">
 				<view class="list-item-wrapper" @tap="handleItemClick(index)">
-					<!-- 批量删除模式下的左侧复选框 -->
-					<view class="checkbox-col" v-if="isBatchMode">
-						<image class="check-icon"
+					<view v-if="isBatchMode" class="checkbox-col">
+						<image
+							class="check-icon"
 							:src="item.selected ? '/static/image/course/check.png' : '/static/image/course/uncheck.png'"
-							mode="aspectFit"></image>
+							mode="aspectFit"
+						></image>
 					</view>
 
 					<view class="swipe-wrap" :class="{ 'in-batch-mode': isBatchMode }">
-						<u-swipe-action :options="swipeOptions" :index="index" @click="handleDelete(index)"
-							:disabled="isBatchMode">
+						<u-swipe-action
+							:options="swipeOptions"
+							:index="index"
+							@click="handleDelete(index)"
+							:disabled="isBatchMode"
+						>
 							<view class="course-item-container">
 								<view class="course-item">
-									<!-- 图片及红心图标 -->
 									<view class="img-wrap">
-										<image class="course-img" :src="item.image" mode="aspectFill"></image>
-										<image class="heart-icon" src="/static/image/course/live.png" mode="aspectFit">
-										</image>
+										<image class="course-img" :src="resolveCourseImage(item.coverImage)" mode="aspectFill"></image>
+										<image class="heart-icon" src="/static/image/course/live.png" mode="aspectFit"></image>
 									</view>
 
-									<!-- 课程信息 -->
 									<view class="course-info">
 										<view class="title-wrap">
 											<view class="tag-list">
-												<view class="tag red-tag" v-if="item.hasHealthTag">养生</view>
-												<view class="tag red-tag" v-if="item.hasIntroTag">入门</view>
+												<view class="tag red-tag">养生</view>
+												<view v-if="item.difficultyName" class="tag red-tag">{{ item.difficultyName }}</view>
 											</view>
 											<text class="course-title">{{ item.title }}</text>
 										</view>
 
 										<view class="course-bottom">
 											<view class="teacher-info">
-												<image class="teacher-avatar" :src="item.teacherAvatar"
-													mode="aspectFill"></image>
-												<text class="teacher-name">{{ item.teacherName }}</text>
+												<image class="teacher-avatar" :src="resolveTeacherAvatar(item.teacherAvatar)" mode="aspectFill"></image>
+												<text class="teacher-name">{{ item.teacherName || '-' }}</text>
 											</view>
 											<view class="student-count">
-												<wd-icon name="user" size="12px" color="#999"
-													custom-class="user-icon"></wd-icon>
-												<text>{{ item.studentCount }}人正在学</text>
+												<wd-icon name="user" size="12px" color="#999" custom-class="user-icon"></wd-icon>
+												<text>{{ formatLearnUserCount(item.learnUserCount) }}人正在学</text>
 											</view>
 										</view>
 									</view>
@@ -62,383 +64,514 @@
 			</block>
 		</scroll-view>
 	</view>
-
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { batchCancelFavoriteCourses, cancelFavoriteCourse, getFavoriteCourses } from './service'
 
-	const statusBarHeight = ref(20);
-	const isBatchMode = ref(false);
+interface FavoriteCourseItem {
+	courseId?: number | null
+	title?: string
+	coverImage?: string
+	levelName?: string
+	difficultyName?: string
+	teacherName?: string
+	teacherAvatar?: string
+	learnUserCount?: number | null
+	selected?: boolean
+}
 
-	// 模拟数据
-	const favoriteList = ref([
-		{
-			id: 1,
-			title: '拍八虚新手入门版',
-			image: '/static/image/home/class.png',
-			hasHealthTag: true,
-			hasIntroTag: false,
-			teacherName: '姓名姓名',
-			teacherAvatar: '/static/image/home/doctor.png',
-			studentCount: 8888,
-			selected: false
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
+const DEFAULT_TEACHER_AVATAR = '/static/image/home/doctor.png'
+
+const statusBarHeight = ref(20)
+const isBatchMode = ref(false)
+const favoriteList = ref<FavoriteCourseItem[]>([])
+
+const swipeOptions = ref([
+	{
+		text: '删除',
+		style: {
+			backgroundColor: '#F53F3F',
+			color: '#fff',
+			fontSize: '28rpx',
+			width: '120rpx',
+			height: '120rpx',
+			borderRadius: '50%',
+			display: 'flex',
+			justifyContent: 'center',
+			alignItems: 'center',
+			margin: '0 30rpx',
 		},
-		{
-			id: 2,
-			title: '拍八虚新手入门版',
-			image: '', // 灰底无图测试
-			hasHealthTag: true,
-			hasIntroTag: true,
-			teacherName: '姓名姓名',
-			teacherAvatar: '/static/image/home/doctor.png',
-			studentCount: 8888,
-			selected: false
-		},
-		{
-			id: 3,
-			title: '拍八虚新手入门版',
-			image: '',
-			hasHealthTag: true,
-			hasIntroTag: false,
-			teacherName: '姓名姓名',
-			teacherAvatar: '/static/image/home/doctor.png',
-			studentCount: 8888,
-			selected: false
-		},
-		{
-			id: 4,
-			title: '拍八虚新手入门版',
-			image: '',
-			hasHealthTag: true,
-			hasIntroTag: false,
-			teacherName: '姓名姓名',
-			teacherAvatar: '/static/image/home/doctor.png',
-			studentCount: 8888,
-			selected: false
+	},
+])
+
+/**
+ * 页面加载时初始化状态栏高度。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+})
+
+/**
+ * 页面回显时刷新收藏列表。
+ */
+onShow(() => {
+	loadFavoriteCourses()
+})
+
+/**
+ * 拉取当前登录用户的收藏课程列表。
+ */
+async function loadFavoriteCourses() {
+	try {
+		const response = await getFavoriteCourses()
+		if (response?.code !== 0) {
+			return
 		}
-	]);
+		favoriteList.value = normalizeFavoriteList(response.data)
+	} catch {
+		// 请求层会统一处理未登录跳转和异常提示,这里不重复 toast。
+	}
+}
 
-	// u-swipe-action 右侧滑出选项
-	const swipeOptions = ref([
-		{
-			text: '删除',
-			style: {
-				backgroundColor: '#F53F3F',
-				color: '#fff',
-				fontSize: '28rpx',
-				width: '120rpx',
-				height: '120rpx',
-				borderRadius: '50%',
-				display: 'flex',
-				justifyContent: 'center',
-				alignItems: 'center',
-				margin: '0 30rpx'
+/**
+ * 将接口返回的收藏列表归一化为页面渲染结构。
+ */
+function normalizeFavoriteList(value: unknown): FavoriteCourseItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const record = (item && typeof item === 'object' ? item : {}) as Partial<FavoriteCourseItem>
+		return {
+			courseId: normalizeOptionalNumber(record.courseId),
+			title: normalizeOptionalText(record.title) || '课程名称',
+			coverImage: normalizeOptionalText(record.coverImage),
+			levelName: normalizeOptionalText(record.levelName),
+			difficultyName: normalizeOptionalText(record.difficultyName),
+			teacherName: normalizeOptionalText(record.teacherName),
+			teacherAvatar: normalizeOptionalText(record.teacherAvatar),
+			learnUserCount: normalizeOptionalNumber(record.learnUserCount),
+			selected: false,
+		}
+	})
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 切换批量模式;确认时执行批量取消。
+ */
+function toggleBatchEdit() {
+	if (!isBatchMode.value) {
+		isBatchMode.value = true
+		return
+	}
+	handleBatchConfirm()
+}
+
+/**
+ * 批量模式下切换勾选;普通模式下跳课程详情。
+ */
+function handleItemClick(index: number) {
+	const currentItem = favoriteList.value[index]
+	if (!currentItem) {
+		return
+	}
+	if (isBatchMode.value) {
+		currentItem.selected = !currentItem.selected
+		return
+	}
+	if (!currentItem.courseId) {
+		return
+	}
+	uni.navigateTo({
+		url: `/pages/courseDetail/courseDetail?id=${currentItem.courseId}`,
+	})
+}
+
+/**
+ * 确认批量取消收藏;未勾选时直接退出批量模式。
+ */
+async function handleBatchConfirm() {
+	const selectedCourseIds = favoriteList.value
+		.filter((item) => item.selected && item.courseId)
+		.map((item) => Number(item.courseId))
+	if (selectedCourseIds.length === 0) {
+		resetBatchMode()
+		return
+	}
+	try {
+		const response = await batchCancelFavoriteCourses(selectedCourseIds)
+		if (response?.code !== 0) {
+			return
+		}
+		favoriteList.value = favoriteList.value.filter((item) => !item.selected)
+		resetBatchMode()
+		uni.showToast({
+			title: '已取消收藏',
+			icon: 'none',
+		})
+	} catch {
+		// 请求层会统一处理异常提示,这里不重复 toast。
+	}
+}
+
+/**
+ * 单条左滑取消收藏。
+ */
+function handleDelete(index: number) {
+	const currentItem = favoriteList.value[index]
+	if (!currentItem || !currentItem.courseId) {
+		return
+	}
+	uni.showModal({
+		title: '提示',
+		content: '确认取消收藏该课程?',
+		success: async (res) => {
+			if (!res.confirm) {
+				return
 			}
-		}
-	]);
-
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
-
-	const goBack = () => {
-		uni.navigateBack();
-	};
-
-	const toggleBatchEdit = () => {
-		isBatchMode.value = !isBatchMode.value;
-		if (!isBatchMode.value) {
-			// 如果取消批量操作,清空所有选中状态
-			favoriteList.value.forEach(item => {
-				item.selected = false;
-			});
-		}
-	};
-
-	const handleItemClick = (index : number) => {
-		if (isBatchMode.value) {
-			favoriteList.value[index].selected = !favoriteList.value[index].selected;
-		} else {
-			// 跳转到详情页逻辑
-			console.log('Go to detail');
-		}
-	};
-
-	const handleDelete = (index : number) => {
-		uni.showModal({
-			title: '提示',
-			content: '确认取消收藏该课程?',
-			success: (res) => {
-				if (res.confirm) {
-					favoriteList.value.splice(index, 1);
-					uni.showToast({
-						title: '已取消收藏',
-						icon: 'none'
-					});
+			try {
+				const response = await cancelFavoriteCourse(currentItem.courseId as number)
+				if (response?.code !== 0) {
+					return
 				}
+				favoriteList.value.splice(index, 1)
+				uni.showToast({
+					title: '已取消收藏',
+					icon: 'none',
+				})
+			} catch {
+				// 请求层会统一处理异常提示,这里不重复 toast。
 			}
-		});
-	};
+		},
+	})
+}
+
+/**
+ * 退出批量模式并清空全部勾选状态。
+ */
+function resetBatchMode() {
+	isBatchMode.value = false
+	favoriteList.value.forEach((item) => {
+		item.selected = false
+	})
+}
+
+/**
+ * 解析课程封面图。
+ */
+function resolveCourseImage(value?: string) {
+	return normalizeOptionalText(value) || DEFAULT_COURSE_IMAGE
+}
+
+/**
+ * 解析讲师头像。
+ */
+function resolveTeacherAvatar(value?: string) {
+	return normalizeOptionalText(value) || DEFAULT_TEACHER_AVATAR
+}
+
+/**
+ * 格式化正在学习人数。
+ */
+function formatLearnUserCount(value?: number | null) {
+	const count = Number(value || 0)
+	if (!Number.isFinite(count)) {
+		return 0
+	}
+	return count
+}
+
+/**
+ * 归一化可选数字字段。
+ */
+function normalizeOptionalNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return null
+	}
+	return numberValue
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
 </script>
 
 <style>
-	page,
-	html {
-		background-color: #fff;
-	}
+page,
+html {
+	background-color: #fff;
+}
 </style>
 
 <style lang="scss" scoped>
-	.container {
-		width: 100vw;
-		height: 100vh;
-		// background-color: #F8F6F4;
-		display: flex;
-		flex-direction: column;
-	}
+.container {
+	width: 100vw;
+	height: 100vh;
+	display: flex;
+	flex-direction: column;
+}
 
-	/* 自定义导航栏 */
-	.custom-navbar {
-		width: 100%;
-		background-color: #FFFFFF;
-		position: relative;
-		z-index: 100;
-	}
+.custom-navbar {
+	width: 100%;
+	background-color: #FFFFFF;
+	position: relative;
+	z-index: 100;
+}
 
-	.nav-content {
-		height: 88rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 32rpx;
-	}
+.nav-content {
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 32rpx;
+}
 
-	.back-icon {
-		width: 40rpx;
-		height: 40rpx;
-	}
+.back-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
 
-	.nav-title {
-		font-size: 36rpx;
-		color: #333333;
-		font-weight: bold;
-		position: absolute;
-		left: 50%;
-		transform: translateX(-50%);
-	}
+.nav-title {
+	font-size: 36rpx;
+	color: #333333;
+	font-weight: bold;
+	position: absolute;
+	left: 50%;
+	transform: translateX(-50%);
+}
 
-	.nav-right-text {
-		font-size: 28rpx;
-		color: #333333;
-	}
+.nav-right-text {
+	font-size: 28rpx;
+	color: #333333;
+}
 
-	/* 收藏列表 */
-	.favorites-list {
-		flex: 1;
-		height: 0;
-		padding: 24rpx 32rpx;
-		box-sizing: border-box;
-		width: 100%;
-	}
+.favorites-list {
+	flex: 1;
+	height: 0;
+	padding: 24rpx 32rpx;
+	box-sizing: border-box;
+	width: 100%;
+}
 
-	.list-item-wrapper {
-		display: flex;
-		align-items: flex-start;
-		width: 100%;
-	}
+.empty-box {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	padding-top: 200rpx;
+}
 
-	.checkbox-col {
-		width: 60rpx;
-		display: flex;
-		justify-content: flex-start;
-		align-items: center;
-		flex-shrink: 0;
-		/* Height set to match the course card to enable vertical centering */
-		height: 200rpx;
-		margin-bottom: 24rpx;
+.empty-text {
+	font-size: 28rpx;
+	color: #999999;
+}
 
-		.check-icon {
-			width: 40rpx;
-			height: 40rpx;
-		}
-	}
+.list-item-wrapper {
+	display: flex;
+	align-items: flex-start;
+	width: 100%;
+}
 
-	.swipe-wrap {
-		margin-bottom: 24rpx;
-		flex: 1;
-		min-width: 0;
-		transition: all 0.3s;
+.checkbox-col {
+	width: 60rpx;
+	display: flex;
+	justify-content: flex-start;
+	align-items: center;
+	flex-shrink: 0;
+	height: 200rpx;
+	margin-bottom: 24rpx;
+}
 
-		&.in-batch-mode {
-			/* You can add specific styles when in batch mode if needed */
-		}
-	}
+.check-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
 
-	// 修改 u-swipe-action 的样式适应圆形删除按钮设计
-	:deep(.u-swipe-action) {
-		background-color: transparent !important;
-		/* 匹配页面底色,让右侧操作区看起来像是悬浮的 */
-	}
+.swipe-wrap {
+	margin-bottom: 24rpx;
+	flex: 1;
+	min-width: 0;
+	transition: all 0.3s;
+}
 
-	/* The actual scroll view container inside u-swipe-action */
-	:deep(.u-swipe-action-item) {
-		background-color: transparent !important;
-	}
+:deep(.u-swipe-action) {
+	background-color: transparent !important;
+}
 
-	:deep(.u-swipe-del) {
-		background-color: transparent !important;
-		display: flex;
-		align-items: center;
-		height: 100%;
-		width: 192rpx !important;
-		height: 204rpx !important;
-		margin: unset !important;
-		background-color: #fff !important;
-		border-radius: unset !important;
-	}
+:deep(.u-swipe-action-item) {
+	background-color: transparent !important;
+}
 
-	:deep(.u-btn-text) {
-		display: none; // 隐藏文字,我们在样式里画个垃圾桶
-	}
+:deep(.u-swipe-del) {
+	background-color: transparent !important;
+	display: flex;
+	align-items: center;
+	height: 100%;
+	width: 192rpx !important;
+	height: 204rpx !important;
+	margin: unset !important;
+	background-color: #fff !important;
+	border-radius: unset !important;
+}
 
-	:deep(.u-swipe-del::after) {
-		content: '';
-		width: 120rpx;
-		height: 120rpx;
-		background-color: transparent;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		margin: 0 32rpx;
-		background-image: url('/static/image/course/delete.png');
-		background-size: 120rpx 120rpx;
-		background-repeat: no-repeat;
-		background-position: center;
-	}
+:deep(.u-btn-text) {
+	display: none;
+}
 
-	.course-item-container {
-		width: 100%;
-		background-color: transparent;
-	}
+:deep(.u-swipe-del::after) {
+	content: '';
+	width: 120rpx;
+	height: 120rpx;
+	background-color: transparent;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin: 0 32rpx;
+	background-image: url('/static/image/course/delete.png');
+	background-size: 120rpx 120rpx;
+	background-repeat: no-repeat;
+	background-position: center;
+}
 
-	.course-item {
-		width: 100%;
-		height: 200rpx;
-		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
-		padding: 24rpx;
-		box-sizing: border-box;
-		display: flex;
-		align-items: center;
-		border-radius: 24rpx;
-	}
+.course-item-container {
+	width: 100%;
+	background-color: transparent;
+}
 
-	.img-wrap {
-		position: relative;
-		width: 152rpx;
-		height: 152rpx;
-		border-radius: 16rpx;
-		margin-right: 24rpx;
-		flex-shrink: 0;
-		background-color: #BDBDBD; // 测试灰底
-		/* Make sure the image wrap doesn't clip the absolutely positioned heart icon */
-		overflow: visible;
-	}
+.course-item {
+	width: 100%;
+	height: 200rpx;
+	background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+	padding: 24rpx;
+	box-sizing: border-box;
+	display: flex;
+	align-items: center;
+	border-radius: 24rpx;
+}
 
-	.course-img {
-		width: 100%;
-		height: 100%;
-		border-radius: 16rpx;
-	}
+.img-wrap {
+	position: relative;
+	width: 152rpx;
+	height: 152rpx;
+	border-radius: 16rpx;
+	margin-right: 24rpx;
+	flex-shrink: 0;
+	background-color: #BDBDBD;
+	overflow: visible;
+}
 
-	.heart-icon {
-		position: absolute;
-		top: 12rpx;
-		right: 12rpx;
-		width: 32rpx;
-		height: 32rpx;
-		z-index: 10;
-	}
+.course-img {
+	width: 100%;
+	height: 100%;
+	border-radius: 16rpx;
+}
 
-	.course-info {
-		flex: 1;
-		height: 152rpx;
-		display: flex;
-		flex-direction: column;
-		justify-content: space-between;
-		min-width: 0;
-	}
+.heart-icon {
+	position: absolute;
+	top: 12rpx;
+	right: 12rpx;
+	width: 32rpx;
+	height: 32rpx;
+	z-index: 10;
+}
 
-	.title-wrap {
-		display: flex;
-		align-items: center;
-		flex-wrap: wrap;
-	}
+.course-info {
+	flex: 1;
+	height: 152rpx;
+	display: flex;
+	flex-direction: column;
+	justify-content: space-between;
+	min-width: 0;
+}
 
-	.tag-list {
-		display: flex;
-		align-items: center;
-	}
+.title-wrap {
+	display: flex;
+	align-items: center;
+	flex-wrap: wrap;
+}
 
-	.tag {
-		font-size: 20rpx;
-		padding: 2rpx 10rpx;
-		border-radius: 8rpx;
-		margin-right: 12rpx;
-		color: #FFFFFF;
-	}
+.tag-list {
+	display: flex;
+	align-items: center;
+}
 
-	.red-tag {
-		background-color: #D32E29;
-	}
+.tag {
+	font-size: 20rpx;
+	padding: 2rpx 10rpx;
+	border-radius: 8rpx;
+	margin-right: 12rpx;
+	color: #FFFFFF;
+}
 
-	.orange-tag {
-		background-color: #C65F34;
-	}
+.red-tag {
+	background-color: #D32E29;
+}
 
-	.course-title {
-		font-size: 32rpx;
-		color: #1A1A1A;
-		font-weight: bold;
-		flex: 1;
-		white-space: nowrap;
-		overflow: hidden;
-		text-overflow: ellipsis;
-	}
+.course-title {
+	font-size: 32rpx;
+	color: #1A1A1A;
+	font-weight: bold;
+	flex: 1;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.course-bottom {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-	}
+.course-bottom {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
 
-	.teacher-info {
-		display: flex;
-		align-items: center;
-	}
+.teacher-info {
+	display: flex;
+	align-items: center;
+	min-width: 0;
+}
 
-	.teacher-avatar {
-		width: 40rpx;
-		height: 40rpx;
-		border-radius: 50%;
-		margin-right: 12rpx;
-		background-color: #f5f5f5;
-	}
+.teacher-avatar {
+	width: 40rpx;
+	height: 40rpx;
+	border-radius: 50%;
+	margin-right: 12rpx;
+	background-color: #f5f5f5;
+}
 
-	.teacher-name {
-		font-size: 26rpx;
-		color: #86909C;
-	}
+.teacher-name {
+	font-size: 26rpx;
+	color: #86909C;
+	max-width: 160rpx;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.student-count {
-		display: flex;
-		align-items: center;
-		font-size: 24rpx;
-		color: #999999;
+.student-count {
+	display: flex;
+	align-items: center;
+	font-size: 24rpx;
+	color: #999999;
+}
 
-		.user-icon {
-			margin-right: 8rpx;
-		}
-	}
-</style>
\ No newline at end of file
+.user-icon {
+	margin-right: 8rpx;
+}
+</style>
diff --git a/traditionalApp/pages/favorites/service.js b/traditionalApp/pages/favorites/service.js
new file mode 100644
index 0000000..c7890bd
--- /dev/null
+++ b/traditionalApp/pages/favorites/service.js
@@ -0,0 +1,22 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取当前登录用户的收藏课程列表。
+ */
+export const getFavoriteCourses = () => {
+	return request.get('/api/app/health-courses/favorites', {}, false, false)
+}
+
+/**
+ * 取消单个课程收藏。
+ */
+export const cancelFavoriteCourse = (courseId) => {
+	return request.delete(`/api/app/health-courses/${courseId}/favorite`, {}, false, true)
+}
+
+/**
+ * 批量取消课程收藏。
+ */
+export const batchCancelFavoriteCourses = (courseIds) => {
+	return request.delete('/api/app/health-courses/favorites', { courseIds }, false, true)
+}
diff --git a/traditionalApp/pages/mine/mine.vue b/traditionalApp/pages/mine/mine.vue
index 58931f0..0d8efb0 100644
--- a/traditionalApp/pages/mine/mine.vue
+++ b/traditionalApp/pages/mine/mine.vue
@@ -3,31 +3,27 @@
 		<view class="top-bg">
 			<image src="/static/image/home/bg.png" mode="widthFix" class="bg-img"></image>
 		</view>
-		<!-- 状态栏占位 -->
 		<view class="status-bar-placeholder" :style="{ height: statusBarHeight + 'px' }"></view>
 
 		<view class="content">
-			<!-- 顶部图标 (通知、设置) -->
 			<view class="top-actions">
 				<image class="action-icon" src="/static/image/mine/notice.png" mode="aspectFit" @tap="goPage('/pages/notice/notice')"></image>
 				<image class="action-icon" src="/static/image/mine/setting.png" mode="aspectFit" @tap="goPage('/pages/settings/settings')"></image>
 			</view>
 
-			<!-- 用户信息 -->
 			<view class="user-info">
 				<view class="avatar-wrap">
-					<image class="avatar" :src="userInfo.avatar" mode="aspectFill"></image>
+					<image class="avatar" :src="displayAvatar" mode="aspectFill"></image>
 					<view class="role-tag" :class="userRole === 'doctor' ? 'doctor-tag' : 'personal-tag'">
 						{{ userRole === 'doctor' ? '医生' : '个人' }}
 					</view>
 				</view>
 				<view class="user-detail">
-					<view class="user-name">{{ userInfo.name }}</view>
-					<view v-if="userRole === 'personal'" class="user-points">积分:{{ userInfo.points }}</view>
+					<view class="user-name">{{ displayName }}</view>
+					<view v-if="userRole === 'personal'" class="user-points">积分:{{ displayPoints }}</view>
 				</view>
 			</view>
 
-			<!-- 会员入口 -->
 			<view class="vip-banner">
 				<image class="vip-bg" src="/static/image/mine/openVip.png" mode="aspectFill"></image>
 				<view class="vip-content">
@@ -35,23 +31,21 @@
 						<view class="vip-title-box">
 							<text class="vip-title">VIP会员</text>
 						</view>
-						<text class="vip-desc">15元起开通畅享养生课</text>
+						<text class="vip-desc">{{ vipDescription }}</text>
 					</view>
 					<view class="vip-btn" @tap="goPage('/pages/vip/vip')">立即开通</view>
 				</view>
 			</view>
 
-			<!-- 操作区 (网格) -->
 			<view class="grid-section card-box">
-				<view class="grid-item" v-for="(item, index) in gridMenuList" :key="index" @tap="handleGridClick(item)">
+				<view class="grid-item" v-for="(item, index) in gridMenuList" :key="`${item.title}-${index}`" @tap="handleGridClick(item)">
 					<image class="grid-icon" :src="item.icon" mode="aspectFit"></image>
 					<text class="grid-title">{{ item.title }}</text>
 				</view>
 			</view>
 
-			<!-- 列表区 -->
 			<view class="list-section card-box">
-				<view class="list-item" v-for="(item, index) in bottomMenuList" :key="index" @tap="goPage(item.url)">
+				<view class="list-item" v-for="(item, index) in bottomMenuList" :key="`${item.title}-${index}`" @tap="goPage(item.url)">
 					<view class="list-left">
 						<image class="list-icon" :class="item.iconClass" :src="item.icon" mode="aspectFit"></image>
 						<text class="list-title">{{ item.title }}</text>
@@ -59,335 +53,541 @@
 					<u-icon name="arrow-right" color="#999" size="16"></u-icon>
 				</view>
 			</view>
-			<!-- ICP备案号 -->
 			<view class="icp-text">ICP备案号:XXXXXXXXXXXXXXXX</view>
 		</view>
 	</view>
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted, computed } from "vue";
-	import { onLoad, onShow } from "@dcloudio/uni-app";
+import { computed, ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { getMemberOpenPage, getUserCenter } from './service'
 
-	const statusBarHeight = ref(0);
-	const userRole = ref('personal'); // 'doctor' or 'personal'
+type UserRole = 'personal' | 'doctor'
 
-	const userInfo = ref({
-		name: '褚冰枫',
-		points: 88888,
-		role: 'personal',
-		avatar: '/static/image/mine/header.png'
-	});
+interface UserCenterData {
+	id?: number | null
+	nickname?: string
+	avatar?: string
+	phone?: string
+	type?: number | null
+	typeText?: string
+	memberExpireAt?: string
+	pointsBalance?: number | null
+	clinicId?: number | null
+	clinicName?: string
+}
 
-	const gridMenuList = computed(() => {
-		if (userRole.value === 'doctor') {
-			return [
-				{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
-				{ title: '会员续费', icon: '/static/image/mine/vip.png', url: '/pages/vip/vip' },
-				{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
-				{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
-				{ title: '我的患者', icon: '/static/image/mine/person.png', url: '/pages/myPatients/myPatients' },
-				{ title: '诊所信息', icon: '/static/image/mine/hosptial.png', url: '/pages/clinicInfo/clinicInfo' },
-				{ title: '二维码', icon: '/static/image/mine/code.png', url: '/pages/inviteUser/inviteUser' },
-			];
-		} else {
-			return [
-				{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
-				{ title: '会员续费', icon: '/static/image/mine/vip.png', url: '/pages/vip/vip' },
-				{ title: '兑换订单', icon: '/static/image/mine/Shopping@2x.png', url: '/pages/myPoints/myPoints' },
-				{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
-				{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
-				{ title: '我的积分', icon: '/static/image/mine/point.png', url: '/pages/myPoints/myPoints' },
-			];
+interface AppUserCache {
+	id?: number | null
+	nickname?: string
+	avatar?: string
+	phone?: string
+	type?: number | null
+	typeText?: string
+	memberExpireAt?: string
+	clinicId?: number | null
+}
+
+interface MenuItem {
+	title: string
+	icon: string
+	url: string
+	iconClass?: string
+}
+
+const statusBarHeight = ref(0)
+const userCenter = ref<UserCenterData>(createDefaultUserCenter())
+const memberMinPrice = ref<number | string | null>(null)
+
+const userRole = computed<UserRole>(() => {
+	return Number(userCenter.value.type) === 1 ? 'doctor' : 'personal'
+})
+const displayName = computed(() => {
+	return normalizeOptionalText(userCenter.value.nickname) || normalizeOptionalText(userCenter.value.phone) || '游客'
+})
+const displayAvatar = computed(() => {
+	return normalizeOptionalText(userCenter.value.avatar) || '/static/image/mine/header.png'
+})
+const displayPoints = computed(() => {
+	const points = Number(userCenter.value.pointsBalance || 0)
+	return Number.isFinite(points) ? points : 0
+})
+const vipDescription = computed(() => {
+	const formattedPrice = formatVipMinPrice(memberMinPrice.value)
+	if (!formattedPrice) {
+		return '开通畅享养生课'
+	}
+	return `${formattedPrice}元起开通畅享养生课`
+})
+
+const gridMenuList = computed<MenuItem[]>(() => {
+	if (userRole.value === 'doctor') {
+		return [
+			{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
+			{ title: '会员续费', icon: '/static/image/mine/vip.png', url: '/pages/vip/vip' },
+			{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
+			{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
+			{ title: '我的患者', icon: '/static/image/mine/person.png', url: '/pages/myPatients/myPatients' },
+			{ title: '诊所信息', icon: '/static/image/mine/hosptial.png', url: '/pages/clinicInfo/clinicInfo' },
+			{ title: '二维码', icon: '/static/image/mine/code.png', url: '/pages/inviteUser/inviteUser' },
+		]
+	}
+	return [
+		{ title: '购课记录', icon: '/static/image/mine/Book@2x.png', url: '/pages/courseRecord/courseRecord' },
+		{ title: '会员续费', icon: '/static/image/mine/vip.png', url: '/pages/vip/vip' },
+		{ title: '兑换订单', icon: '/static/image/mine/Shopping@2x.png', url: '/pages/exchangeRecord/exchangeRecord' },
+		{ title: '收藏列表', icon: '/static/image/mine/list.png', url: '/pages/favorites/favorites' },
+		{ title: '我的下载', icon: '/static/image/mine/downLoad.png', url: '/pages/downloads/downloads' },
+		{ title: '我的积分', icon: '/static/image/mine/point.png', url: '/pages/point/point' },
+	]
+})
+
+const bottomMenuList = computed<MenuItem[]>(() => {
+	const baseList = [
+		{ title: '我的课程', icon: '/static/image/mine/myClass.png', url: '/pages/myCourses/myCourses', iconClass: 'icon-myclass' },
+		{ title: '帮助中心', icon: '/static/image/mine/help.png', url: '/pages/helpCenter/helpCenter', iconClass: 'icon-help' },
+		{ title: '意见反馈', icon: '/static/image/mine/message.png', url: '/pages/feedback/feedback', iconClass: 'icon-feedback' },
+	]
+	const agreementList = [
+		{ title: '用户协议', icon: '/static/image/mine/coustom.png', url: '/pages/protocol/protocol?type=user', iconClass: 'icon-user-agree' },
+		{ title: '隐私协议', icon: '/static/image/mine/agree.png', url: '/pages/protocol/protocol?type=privacy', iconClass: 'icon-privacy' },
+	]
+	if (userRole.value === 'doctor') {
+		return [
+			...baseList,
+			{ title: '平台资质', icon: '/static/image/mine/chimaping.png', url: '/pages/qualification/qualification', iconClass: 'icon-platform' },
+			...agreementList,
+		]
+	}
+	return [...baseList, ...agreementList]
+})
+
+/**
+ * 创建个人中心默认数据,便于首屏兜底。
+ */
+function createDefaultUserCenter(): UserCenterData {
+	return {
+		id: null,
+		nickname: '',
+		avatar: '',
+		phone: '',
+		type: 0,
+		typeText: '个人',
+		memberExpireAt: '',
+		pointsBalance: 0,
+		clinicId: null,
+		clinicName: '',
+	}
+}
+
+/**
+ * 页面加载时初始化状态栏高度并回显本地缓存。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+	syncCachedUserInfo()
+	loadMemberOpenPage()
+})
+
+/**
+ * 页面回显时重新拉取个人中心聚合数据,保证头像和积分同步刷新。
+ */
+onShow(() => {
+	syncCachedUserInfo()
+	loadUserCenter()
+	loadMemberOpenPage()
+})
+
+/**
+ * 拉取当前登录用户的个人中心聚合数据。
+ */
+async function loadUserCenter() {
+	try {
+		const response = await getUserCenter()
+		if (response?.code !== 0 || !response.data) {
+			return
 		}
-	});
+		userCenter.value = normalizeUserCenter(response.data)
+		cacheUserCenter(userCenter.value)
+	} catch {
+		// 未登录或网络异常时由统一请求层处理跳转,这里不重复 toast。
+	}
+}
 
-	const bottomMenuList = computed(() => {
-		const baseList = [
-			{ title: '我的课程', icon: '/static/image/mine/myClass.png', url: '/pages/myCourses/myCourses', iconClass: 'icon-myclass' },
-			{ title: '帮助中心', icon: '/static/image/mine/help.png', url: '/pages/helpCenter/helpCenter', iconClass: 'icon-help' },
-			{ title: '意见反馈', icon: '/static/image/mine/message.png', url: '/pages/feedback/feedback', iconClass: 'icon-feedback' },
-		];
-
-		const agreementList = [
-			{ title: '用户协议', icon: '/static/image/mine/coustom.png', url: '/pages/protocol/protocol?type=user', iconClass: 'icon-user-agree' },
-			{ title: '隐私协议', icon: '/static/image/mine/agree.png', url: '/pages/protocol/protocol?type=privacy', iconClass: 'icon-privacy' },
-		];
-
-		if (userRole.value === 'doctor') {
-			return [
-				...baseList,
-				{ title: '平台资质', icon: '/static/image/mine/chimaping.png', url: '/pages/qualification/qualification', iconClass: 'icon-platform' },
-				...agreementList
-			];
-		} else {
-			return [
-				...baseList,
-				...agreementList
-			];
+/**
+ * 拉取会员开通页最低价格,用于个人中心会员横幅文案展示。
+ */
+async function loadMemberOpenPage() {
+	try {
+		const response = await getMemberOpenPage()
+		if (response?.code !== 0 || !response.data) {
+			memberMinPrice.value = null
+			return
 		}
-	});
+		memberMinPrice.value = response.data.minPrice ?? null
+	} catch {
+		// 文案接口失败时使用兜底文案,避免个人中心反复提示。
+		memberMinPrice.value = null
+	}
+}
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+/**
+ * 使用本地登录缓存兜底首屏展示,减少页面闪烁。
+ */
+function syncCachedUserInfo() {
+	const cachedUser = uni.getStorageSync('appUserInfo')
+	if (!cachedUser || typeof cachedUser !== 'object') {
+		userCenter.value = createDefaultUserCenter()
+		return
+	}
+	userCenter.value = normalizeUserCenter(cachedUser)
+}
 
-	const handleGridClick = (item : any) => {
-		if (item.url) {
-			uni.navigateTo({
-				url: item.url
-			});
-		}
-	};
+/**
+ * 将接口或缓存中的用户信息归一化为页面可直接消费的结构。
+ */
+function normalizeUserCenter(value: unknown): UserCenterData {
+	const source = (value && typeof value === 'object' ? value : {}) as Partial<UserCenterData>
+	const type = Number(source.type)
+	return {
+		id: Number.isFinite(Number(source.id)) ? Number(source.id) : null,
+		nickname: normalizeOptionalText(source.nickname),
+		avatar: normalizeOptionalText(source.avatar),
+		phone: normalizeOptionalText(source.phone),
+		type: Number.isFinite(type) ? type : 0,
+		typeText: normalizeOptionalText(source.typeText),
+		memberExpireAt: normalizeOptionalText(source.memberExpireAt),
+		pointsBalance: Number.isFinite(Number(source.pointsBalance)) ? Number(source.pointsBalance) : 0,
+		clinicId: Number.isFinite(Number(source.clinicId)) ? Number(source.clinicId) : null,
+		clinicName: normalizeOptionalText(source.clinicName),
+	}
+}
 
-	const goPage = (url : string) => {
-		uni.navigateTo({
-			url
-		});
-	};
+/**
+ * 将个人中心接口返回的关键用户字段同步回本地登录缓存。
+ */
+function cacheUserCenter(centerData: UserCenterData) {
+	const cachedUser = uni.getStorageSync('appUserInfo')
+	const nextCache: AppUserCache = {
+		...(cachedUser && typeof cachedUser === 'object' ? cachedUser : {}),
+		id: centerData.id,
+		nickname: centerData.nickname,
+		avatar: centerData.avatar,
+		phone: centerData.phone,
+		type: centerData.type,
+		typeText: centerData.typeText,
+		memberExpireAt: centerData.memberExpireAt,
+		clinicId: centerData.clinicId,
+	}
+	uni.setStorageSync('appUserInfo', nextCache)
+}
+
+/**
+ * 点击宫格菜单时跳转目标页面。
+ */
+function handleGridClick(item: MenuItem) {
+	if (!item.url) {
+		return
+	}
+	goPage(item.url)
+}
+
+/**
+ * 统一跳转个人中心相关页面。
+ */
+function goPage(url: string) {
+	if (!url) {
+		return
+	}
+	uni.navigateTo({
+		url,
+	})
+}
+
+/**
+ * 将会员最低价格式化为横幅文案所需的金额字符串。
+ */
+function formatVipMinPrice(value: number | string | null | undefined) {
+	const price = Number(value)
+	if (!Number.isFinite(price) || price <= 0) {
+		return ''
+	}
+	const fixed = price.toFixed(2)
+	return fixed.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1')
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
 </script>
 
 <style lang="scss" scoped>
-	page {
-		background-color: #F8F6F4;
-	}
+page {
+	background-color: #F8F6F4;
+}
 
-	.container {
+.container {
+	width: 750rpx;
+	min-height: 100vh;
+	position: relative;
+	background-color: #F8F6F4;
+	padding-bottom: env(safe-area-inset-bottom);
+}
+
+.top-bg {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 750rpx;
+	z-index: 0;
+
+	.bg-img {
 		width: 750rpx;
-		min-height: 100vh;
+		height: auto;
+	}
+}
+
+.status-bar-placeholder {
+	width: 100%;
+	position: relative;
+	z-index: 10;
+}
+
+.content {
+	position: relative;
+	z-index: 10;
+	padding: 0 32rpx;
+	padding-bottom: 40rpx;
+}
+
+.top-actions {
+	display: flex;
+	justify-content: flex-end;
+	align-items: center;
+	padding-top: 20rpx;
+	gap: 32rpx;
+
+	.action-icon {
+		width: 48rpx;
+		height: 48rpx;
+	}
+}
+
+.user-info {
+	display: flex;
+	align-items: center;
+	margin-top: 40rpx;
+
+	.avatar-wrap {
 		position: relative;
-		background-color: #F8F6F4;
-		padding-bottom: env(safe-area-inset-bottom);
-	}
+		width: 120rpx;
+		height: 120rpx;
 
-	.top-bg {
-		position: absolute;
-		top: 0;
-		left: 0;
-		width: 750rpx;
-		z-index: 0;
-
-		.bg-img {
-			width: 750rpx;
-			height: auto;
-		}
-	} 
-
-	.status-bar-placeholder {
-		width: 100%;
-		position: relative;
-		z-index: 10;
-	}
-
-	.content {
-		position: relative;
-		z-index: 10;
-		padding: 0 32rpx;
-		padding-bottom: 40rpx;
-	}
-
-	.top-actions {
-		display: flex;
-		justify-content: flex-end;
-		align-items: center;
-		padding-top: 20rpx;
-		gap: 32rpx;
-
-		.action-icon {
-			width: 48rpx;
-			height: 48rpx;
-		}
-	}
-
-	.user-info {
-		display: flex;
-		align-items: center;
-		margin-top: 40rpx;
-
-		.avatar-wrap {
-			position: relative;
+		.avatar {
 			width: 120rpx;
 			height: 120rpx;
-
-			.avatar {
-				width: 120rpx;
-				height: 120rpx;
-				border-radius: 50%;
-				background-color: #eee;
-			}
-
-			.role-tag {
-				position: absolute;
-				bottom: -10rpx;
-				left: 50%;
-				transform: translateX(-50%);
-				color: #fff;
-				font-size: 20rpx;
-				padding: 2rpx 12rpx;
-				border-radius: 10rpx;
-				white-space: nowrap;
-				z-index: 1;
-			}
-
-			.doctor-tag {
-				background-color: #E64340;
-			}
-
-			.personal-tag {
-				background-color: #FF5A5F;
-			}
+			border-radius: 50%;
+			background-color: #eee;
 		}
 
-		.user-detail {
-			margin-left: 32rpx;
-			display: flex;
-			flex-direction: column;
+		.role-tag {
+			position: absolute;
+			bottom: -10rpx;
+			left: 50%;
+			transform: translateX(-50%);
+			color: #fff;
+			font-size: 20rpx;
+			padding: 2rpx 12rpx;
+			border-radius: 10rpx;
+			white-space: nowrap;
+			z-index: 1;
+		}
 
-			.user-name {
-				font-size: 40rpx;
-				color: #1A1A1A;
-				font-weight: 500;
-			}
+		.doctor-tag {
+			background-color: #E64340;
+		}
 
-			.user-points {
-				font-size: 24rpx;
-				color: #666;
-				margin-top: 8rpx;
-			}
+		.personal-tag {
+			background-color: #FF5A5F;
 		}
 	}
 
-	.vip-banner {
+	.user-detail {
+		margin-left: 32rpx;
+		display: flex;
+		flex-direction: column;
+
+		.user-name {
+			font-size: 40rpx;
+			color: #1A1A1A;
+			font-weight: 500;
+		}
+
+		.user-points {
+			font-size: 24rpx;
+			color: #666;
+			margin-top: 8rpx;
+		}
+	}
+}
+
+.vip-banner {
+	position: relative;
+	width: 100%;
+	height: 140rpx;
+	border-radius: 16rpx;
+	overflow: hidden;
+	margin-top: 48rpx;
+	margin-bottom: 32rpx;
+
+	.vip-bg {
+		position: absolute;
+		width: 100%;
+		height: 100%;
+		top: 0;
+		left: 0;
+	}
+
+	.vip-content {
 		position: relative;
 		width: 100%;
-		height: 140rpx;
-		border-radius: 16rpx;
-		overflow: hidden;
-		margin-top: 48rpx;
-		margin-bottom: 32rpx;
+		height: 100%;
+		box-sizing: border-box;
+		padding: 24rpx 32rpx 24rpx 140rpx;
+		display: flex;
+		justify-content: space-between;
+		align-items: center;
 
-		.vip-bg {
-			position: absolute;
-			width: 100%;
-			height: 100%;
-			top: 0;
-			left: 0;
-		}
-
-		.vip-content {
-			position: relative;
-			width: 100%;
-			height: 100%;
-			box-sizing: border-box;
-			padding: 24rpx 32rpx 24rpx 140rpx;
-			display: flex;
-			justify-content: space-between;
-			align-items: center;
-
-			.vip-left {
-				display: flex;
-				flex-direction: column;
-				justify-content: center;
-
-				.vip-title {
-					font-size: 32rpx;
-					font-weight: bold;
-					color: #AA5200;
-					margin-bottom: 4rpx;
-				}
-
-				.vip-desc {
-					font-size: 24rpx;
-					color: #AA5200;
-					opacity: 0.8;
-				}
-			}
-
-			.vip-btn {
-				background: linear-gradient(180deg, #FFD99B 0%, #FFB13C 100%);
-				color: #AA5200;
-				font-size: 24rpx;
-				font-weight: bold;
-				padding: 12rpx 24rpx;
-				border-radius: 30rpx;
-			}
-		}
-	}
-
-	.card-box {
-		background-color: #FFFFFF;
-		border-radius: 16rpx;
-		padding: 32rpx;
-		margin-bottom: 24rpx;
-	}
-
-	.grid-section {
-		display: grid;
-		grid-template-columns: repeat(4, 1fr);
-		gap: 40rpx 0;
-
-		.grid-item {
+		.vip-left {
 			display: flex;
 			flex-direction: column;
-			align-items: center;
+			justify-content: center;
 
-			.grid-icon {
-				width: 56rpx;
-				height: 56rpx;
-				margin-bottom: 16rpx;
+			.vip-title {
+				font-size: 32rpx;
+				font-weight: bold;
+				color: #AA5200;
+				margin-bottom: 4rpx;
 			}
 
-			.grid-title {
-				font-size: 26rpx;
-				color: #1A1A1A;
+			.vip-desc {
+				font-size: 24rpx;
+				color: #AA5200;
+				opacity: 0.8;
 			}
 		}
+
+		.vip-btn {
+			background: linear-gradient(180deg, #FFD99B 0%, #FFB13C 100%);
+			color: #AA5200;
+			font-size: 24rpx;
+			font-weight: bold;
+			padding: 12rpx 24rpx;
+			border-radius: 30rpx;
+		}
 	}
+}
 
-	.list-section {
-		padding: 0 32rpx;
+.card-box {
+	background-color: #FFFFFF;
+	border-radius: 16rpx;
+	padding: 32rpx;
+	margin-bottom: 24rpx;
+}
 
-		.list-item {
+.grid-section {
+	display: grid;
+	grid-template-columns: repeat(4, 1fr);
+	gap: 40rpx 0;
+
+	.grid-item {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+
+		.grid-icon {
+			width: 56rpx;
+			height: 56rpx;
+			margin-bottom: 16rpx;
+		}
+
+		.grid-title {
+			font-size: 26rpx;
+			color: #1A1A1A;
+		}
+	}
+}
+
+.list-section {
+	padding: 0 32rpx;
+
+	.list-item {
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		height: 110rpx;
+		border-bottom: 1px solid #F2F3F5;
+
+		&:last-child {
+			border-bottom: none;
+		}
+
+		.list-left {
 			display: flex;
 			align-items: center;
-			justify-content: space-between;
-			height: 110rpx;
-			border-bottom: 1px solid #F2F3F5;
 
-			&:last-child {
-				border-bottom: none;
+			.list-icon {
+				width: 40rpx;
+				height: 40rpx;
+
+				&.icon-help {
+					width: 34rpx;
+					height: 34rpx;
+				}
+
+				&.icon-feedback {
+					width: 34rpx;
+					height: 34rpx;
+				}
+
+				&.icon-platform {
+					width: 34rpx;
+					height: 34rpx;
+				}
+
+				&.icon-user-agree {
+					width: 30rpx;
+					height: 30rpx;
+				}
+
+				&.icon-privacy {
+					width: 27rpx;
+					height: 33rpx;
+				}
 			}
 
-			.list-left {
-				display: flex;
-				align-items: center;
-
-				.list-icon {
-					width: 40rpx;
-					height: 40rpx;
-					
-					&.icon-help { width: 34rpx; height: 34rpx; }
-					&.icon-feedback { width: 34rpx; height: 34rpx; }
-					&.icon-platform { width: 34rpx; height: 34rpx; }
-					&.icon-user-agree { width: 30rpx; height: 30rpx; }
-					&.icon-privacy { width: 27rpx; height: 33rpx; }
-				}
-
-				.list-title {
-					font-size: 32rpx;
-					color: #1A1A1A;
-					margin-left: 22rpx;
-				}
+			.list-title {
+				font-size: 32rpx;
+				color: #1A1A1A;
+				margin-left: 22rpx;
 			}
 		}
 	}
+}
 
-	.icp-text {
-		font-size: 28rpx;
-		color: #86909C;
-		text-align: center;
-		margin-top: 32rpx;
-		padding-bottom: 20rpx;
-	}
+.icp-text {
+	font-size: 28rpx;
+	color: #86909C;
+	text-align: center;
+	margin-top: 32rpx;
+	padding-bottom: 20rpx;
+}
 </style>
diff --git a/traditionalApp/pages/mine/service.js b/traditionalApp/pages/mine/service.js
index 6aa6b01..111066d 100644
--- a/traditionalApp/pages/mine/service.js
+++ b/traditionalApp/pages/mine/service.js
@@ -1,12 +1,15 @@
 import request from '@/utils/request.js'
 
-//人中心--用户信息
-export const getUserInfo = (data) => {
-	return request.get(`/userIndex/getUserInfo`, )
+/**
+ * 获取 APP 个人中心聚合数据。
+ */
+export const getUserCenter = () => {
+	return request.get('/api/app/user-center', {}, false, false)
 }
 
-
-//协议配置
-export const htmlConfig = (type) => {
-	return request.get(`/html-config/${type}`, )
-}
\ No newline at end of file
+/**
+ * 获取 APP 会员开通页聚合数据。
+ */
+export const getMemberOpenPage = () => {
+	return request.get('/api/app/member/open-page', {}, false, false)
+}
diff --git a/traditionalApp/pages/myCourseDetail/myCourseDetail.vue b/traditionalApp/pages/myCourseDetail/myCourseDetail.vue
index 0797765..8c467d7 100644
--- a/traditionalApp/pages/myCourseDetail/myCourseDetail.vue
+++ b/traditionalApp/pages/myCourseDetail/myCourseDetail.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -9,39 +8,43 @@
 			</view>
 		</view>
 
-		<!-- 视频播放区域 (占位) -->
 		<view class="video-section">
-			<!-- 这里用图片模拟视频播放器 -->
-			<image class="video-poster" src="/static/image/course/bg.png" mode="aspectFill"></image>
-			<view class="play-btn-wrap">
-				<image class="play-icon" src="/static/image/course/live.png" mode="aspectFit"></image>
-			</view>
-			<!-- 底部控制栏模拟 -->
-			<view class="video-controls">
-				<view class="controls-left">
-					<view class="control-icon play-small"></view>
-					<view class="control-icon next-small"></view>
-					<view class="control-icon sound-small"></view>
-				</view>
-				<view class="controls-right">
-					<view class="control-icon setting-small"></view>
-					<view class="control-icon fullscreen-small"></view>
+			<view v-if="currentVideoUrl" class="video-player-wrapper">
+				<video
+					id="my-course-video-player"
+					class="video-player"
+					:src="currentVideoUrl"
+					:poster="videoPoster"
+					:controls="true"
+					:autoplay="isAppPlus"
+					object-fit="contain"
+					play-btn-position="center"
+					@loadedmetadata="handleVideoLoadedMetadata"
+					@timeupdate="handleVideoTimeUpdate"
+					@play="handleVideoPlay"
+					@pause="handleVideoPause"
+					@ended="handleVideoEnded"
+					@error="handleVideoError"
+				></video>
+				<view v-if="showPlaybackRetry" class="video-retry-mask" @tap.stop="retryPlayback">
+					<text class="video-retry-text">点击开始播放</text>
 				</view>
 			</view>
+			<image v-else class="video-poster" :src="videoPoster" mode="aspectFill"></image>
 		</view>
 
-		<!-- 课程列表头部 -->
 		<view class="list-header">
 			<text class="list-title">课程列表</text>
-			<text class="list-desc">共12课 | 15小时</text>
+			<text class="list-desc">共{{ detail.totalLessons || 0 }}课 | {{ detail.totalDurationText || '0分钟' }}</text>
 		</view>
 
-		<!-- 课程列表 -->
 		<scroll-view scroll-y class="lesson-list">
-			<block v-for="(item, index) in lessonList" :key="index">
-				<view class="lesson-item" :class="{ active: item.isActive }">
+			<view v-if="!loading && lessonList.length === 0" class="empty-box">
+				<text class="empty-text">暂无课程章节</text>
+			</view>
+			<block v-for="item in lessonList" :key="item.videoId">
+				<view class="lesson-item" :class="{ active: item.isActive }" @tap="switchVideo(item.videoId)">
 					<view class="item-left">
-						<!-- 正在播放的动画图标 -->
 						<view class="playing-icon" v-if="item.isActive">
 							<view class="bar bar1"></view>
 							<view class="bar bar2"></view>
@@ -53,9 +56,13 @@
 					</view>
 					<view class="item-right">
 						<text class="lesson-duration" :class="{ 'active-text': item.isActive }">{{ item.duration }}</text>
-						<image class="download-icon" :src="item.isDownloaded ? '/static/image/course/grayDownLoad.png' : '/static/image/course/redDownload@2x.png'" mode="aspectFit"></image>
+						<image
+							class="download-icon"
+							:src="item.isDownloaded ? '/static/image/course/grayDownLoad.png' : '/static/image/course/redDownload@2x.png'"
+							mode="aspectFit"
+							@tap.stop="handleVideoDownload(item)"
+						></image>
 					</view>
-					<!-- 进度条 -->
 					<view class="progress-wrap" v-if="item.progress > 0">
 						<view class="progress-bar">
 							<view class="progress-inner" :style="{ width: item.progress + '%' }"></view>
@@ -66,13 +73,12 @@
 			</block>
 		</scroll-view>
 
-		<!-- 底部操作栏 -->
 		<view class="bottom-bar">
-			<view class="action-btn">
-				<image class="action-icon" src="/static\image\healthCourses\listen.png" mode="aspectFit"></image>
+			<view class="action-btn" @tap="handleConsult">
+				<image class="action-icon" src="/static/image/healthCourses/listen.png" mode="aspectFit"></image>
 				<text class="action-text">咨询</text>
 			</view>
-			<view class="action-btn">
+			<view class="action-btn" @tap="handleCurrentVideoDownload">
 				<image class="action-icon" src="/static/image/course/download.png" mode="aspectFit"></image>
 				<text class="action-text">下载</text>
 			</view>
@@ -82,31 +88,895 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { computed, getCurrentInstance, ref } from 'vue'
+import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
+import { getMyHealthCourseDetail, uploadMyHealthCoursePlayRecord } from './service'
+import { clearDownloadTask, listDownloadTasks, upsertDownloadTask } from '@/utils/download-task-storage.js'
+import { reportHealthCourseDownload } from '@/pages/courseDetail/service'
 
-	const statusBarHeight = ref(20);
+interface MyHealthCourseVideoItem {
+	videoId: number
+	chapterName: string
+	durationSeconds: number
+	durationText: string
+	videoUrl: string
+	completed: boolean
+	progressPercent: number
+	lastPlayPositionSeconds: number
+	downloadable: boolean
+}
 
-	const lessonList = ref([
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: false, progress: 0, isDownloaded: true },
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: true, progress: 0, isDownloaded: false },
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: false, progress: 0, isDownloaded: false },
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: false, progress: 0, isDownloaded: true },
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: false, progress: 40, isDownloaded: false },
-		{ title: '1、食品安全风险分级管理 (1)', duration: '00:20:42', isActive: false, progress: 0, isDownloaded: false }
-	]);
+interface MyHealthCourseDetail {
+	courseId: number
+	title: string
+	coverImage: string
+	courseTitle: string
+	totalLessons: number
+	totalDurationText: string
+	currentVideoId: number
+	servicePhone: string
+	introAvailable: boolean
+	videos: MyHealthCourseVideoItem[]
+}
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+interface LessonViewItem {
+	videoId: number
+	title: string
+	duration: string
+	progress: number
+	isDownloaded: boolean
+	isActive: boolean
+	downloadable: boolean
+	videoUrl: string
+	durationSeconds: number
+}
 
-	const goBack = () => {
-		uni.navigateBack();
-	};
+interface LocalDownloadTask {
+	clientTaskId: string
+	courseId: number
+	courseVideoId: number
+	title: string
+	subtitle: string
+	coverImage: string
+	durationSeconds: number
+	durationText: string
+	videoUrl: string
+	progress: number
+	status: 'downloading'
+	updatedAt: string
+}
 
-	const goCourseIntro = () => {
-		console.log('Go to course intro');
-	};
+type VideoMetadataEvent = {
+	detail?: {
+		duration?: number
+	}
+}
+
+type VideoTimeUpdateEvent = {
+	detail?: {
+		currentTime?: number
+	}
+}
+
+const VIDEO_PLAYER_ID = 'my-course-video-player'
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
+let isAppPlus = false
+let isH5 = false
+// #ifdef APP-PLUS
+isAppPlus = true
+// #endif
+// #ifdef H5
+isH5 = true
+// #endif
+
+const statusBarHeight = ref(20)
+const loading = ref(false)
+const courseId = ref('')
+const detail = ref<MyHealthCourseDetail>(createDefaultDetail())
+const activeVideoId = ref<number>(0)
+const isVideoReady = ref(false)
+const isPlaybackStarted = ref(false)
+const playbackFailed = ref(false)
+const isSeekingPlayback = ref(false)
+const mediaDurationSeconds = ref(0)
+const currentWatchStartedAt = ref(0)
+const currentPlaybackSeconds = ref(0)
+const lastReportedPlaybackSeconds = ref(0)
+const backSubmitting = ref(false)
+const watchSegmentSubmitting = ref(false)
+const instance = getCurrentInstance()
+let videoContext: ReturnType<typeof uni.createVideoContext> | null = null
+let watchHeartbeatTimer: ReturnType<typeof setInterval> | null = null
+
+/**
+ * 生成默认详情数据,避免页面空对象访问。
+ */
+function createDefaultDetail(): MyHealthCourseDetail {
+	return {
+		courseId: 0,
+		title: '',
+		coverImage: '',
+		courseTitle: '',
+		totalLessons: 0,
+		totalDurationText: '',
+		currentVideoId: 0,
+		servicePhone: '',
+		introAvailable: false,
+		videos: [],
+	}
+}
+
+const currentVideo = computed(() => {
+	return detail.value.videos.find((item) => item.videoId === activeVideoId.value) || null
+})
+
+const currentVideoUrl = computed(() => currentVideo.value?.videoUrl || '')
+const videoPoster = computed(() => detail.value.coverImage || DEFAULT_COURSE_IMAGE)
+const lessonList = computed<LessonViewItem[]>(() => {
+	const downloadingTasks = listCurrentDownloadTasks()
+	return detail.value.videos.map((item) => ({
+		videoId: item.videoId,
+		title: item.chapterName,
+		duration: item.durationText,
+		progress: normalizePercent(item.progressPercent),
+		isDownloaded: item.downloadable && hasDownloadedTask(downloadingTasks, item.videoId),
+		isActive: item.videoId === activeVideoId.value,
+		downloadable: item.downloadable,
+		videoUrl: item.videoUrl,
+		durationSeconds: item.durationSeconds,
+	}))
+})
+const showPlaybackRetry = computed(() => {
+	if (!currentVideoUrl.value || !isVideoReady.value) {
+		return false
+	}
+	return playbackFailed.value || (isH5 && !isPlaybackStarted.value)
+})
+
+/**
+ * 页面加载时初始化参数与状态栏高度。
+ */
+onLoad((options) => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+	courseId.value = typeof options?.courseId === 'string' ? options.courseId : ''
+})
+
+/**
+ * 页面显示时刷新详情,保证章节和进度为最新状态。
+ */
+onShow(() => {
+	loadMyHealthCourseDetail()
+})
+
+/**
+ * 页面隐藏时补提最后一段播放区间。
+ */
+onHide(() => {
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment()
+})
+
+/**
+ * 页面卸载时补提最后一段播放区间。
+ */
+onUnload(() => {
+	unbindH5PlaybackLifecycle()
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment()
+})
+
+/**
+ * 拉取我的课程详情。
+ */
+async function loadMyHealthCourseDetail() {
+	if (!courseId.value) {
+		return
+	}
+	loading.value = true
+	await submitCurrentWatchSegment()
+	resetPlaybackState()
+	try {
+		const response = await getMyHealthCourseDetail(courseId.value)
+		if (response?.code !== 0 || !response.data) {
+			return
+		}
+		detail.value = normalizeDetail(response.data)
+		activeVideoId.value = resolveInitialVideoId(detail.value)
+		initVideoContext()
+		bindH5PlaybackLifecycle()
+	} catch {
+		// 请求层已统一处理异常提示。
+	} finally {
+		loading.value = false
+	}
+}
+
+/**
+ * 将接口详情归一化为页面结构。
+ */
+function normalizeDetail(value: unknown): MyHealthCourseDetail {
+	const record = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>
+	return {
+		courseId: normalizeNumber(record.courseId),
+		title: normalizeText(record.title),
+		coverImage: normalizeText(record.coverImage),
+		courseTitle: normalizeText(record.courseTitle),
+		totalLessons: normalizeNumber(record.totalLessons),
+		totalDurationText: normalizeText(record.totalDurationText),
+		currentVideoId: normalizeNumber(record.currentVideoId),
+		servicePhone: normalizeText(record.servicePhone),
+		introAvailable: !!record.introAvailable,
+		videos: normalizeVideos(record.videos),
+	}
+}
+
+/**
+ * 归一化章节列表。
+ */
+function normalizeVideos(value: unknown): MyHealthCourseVideoItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const video = (item && typeof item === 'object' ? item : {}) as Record<string, unknown>
+		return {
+			videoId: normalizeNumber(video.videoId),
+			chapterName: normalizeText(video.chapterName) || '课程章节',
+			durationSeconds: normalizeNumber(video.durationSeconds),
+			durationText: normalizeText(video.durationText) || '00:00:00',
+			videoUrl: normalizeText(video.videoUrl),
+			completed: !!video.completed,
+			progressPercent: normalizePercent(video.progressPercent),
+			lastPlayPositionSeconds: normalizeNumber(video.lastPlayPositionSeconds),
+			downloadable: !!video.downloadable,
+		}
+	}).filter((item) => item.videoId > 0)
+}
+
+/**
+ * 解析默认播放章节,优先续播章节。
+ */
+function resolveInitialVideoId(detailValue: MyHealthCourseDetail) {
+	if (detailValue.currentVideoId > 0) {
+		return detailValue.currentVideoId
+	}
+	return detailValue.videos[0]?.videoId || 0
+}
+
+/**
+ * 初始化视频上下文,便于控制续播和自动播放。
+ */
+function initVideoContext() {
+	videoContext = uni.createVideoContext(VIDEO_PLAYER_ID, instance?.proxy)
+}
+
+/**
+ * 重置播放状态。
+ */
+function resetPlaybackState() {
+	isVideoReady.value = false
+	isPlaybackStarted.value = false
+	playbackFailed.value = false
+	isSeekingPlayback.value = false
+	mediaDurationSeconds.value = 0
+	currentWatchStartedAt.value = 0
+	currentPlaybackSeconds.value = 0
+	lastReportedPlaybackSeconds.value = 0
+	clearWatchHeartbeatTimer()
+}
+
+/**
+ * 元数据加载完成后执行续播。
+ */
+function handleVideoLoadedMetadata(event: VideoMetadataEvent) {
+	isVideoReady.value = true
+	mediaDurationSeconds.value = normalizeNumber(event?.detail?.duration) || currentVideo.value?.durationSeconds || 0
+	seekToResumePosition()
+	retryPlayback()
+}
+
+/**
+ * 播放过程中持续记录当前时间,确保暂停或离开时能上报真实续播点。
+ */
+function handleVideoTimeUpdate(event: VideoTimeUpdateEvent) {
+	if (isSeekingPlayback.value) {
+		return
+	}
+	const nextPlaybackSeconds = Math.max(Math.floor(Number(event?.detail?.currentTime || 0)), 0)
+	const previousPlaybackSeconds = currentPlaybackSeconds.value
+	currentPlaybackSeconds.value = nextPlaybackSeconds
+	if (nextPlaybackSeconds > previousPlaybackSeconds) {
+		isPlaybackStarted.value = true
+		playbackFailed.value = false
+		startWatchSegment()
+		startWatchHeartbeat()
+	}
+}
+
+/**
+ * 播放开始时记录片段开始位置。
+ */
+function handleVideoPlay() {
+	isPlaybackStarted.value = true
+	playbackFailed.value = false
+	if (!isSeekingPlayback.value) {
+		startWatchSegment()
+		startWatchHeartbeat()
+	}
+}
+
+/**
+ * 播放暂停时提交已观看区间。
+ */
+function handleVideoPause() {
+	if (isSeekingPlayback.value) {
+		return
+	}
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment()
+}
+
+/**
+ * 播放结束时提交最后一段区间,并把当前位置归零。
+ */
+function handleVideoEnded() {
+	currentPlaybackSeconds.value = mediaDurationSeconds.value
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment(mediaDurationSeconds.value)
+	updateActiveVideoProgress(100, true, 0)
+}
+
+/**
+ * 视频报错时展示点击重试入口。
+ */
+function handleVideoError() {
+	playbackFailed.value = true
+	clearWatchHeartbeatTimer()
+}
+
+/**
+ * 重试当前章节播放。
+ */
+function retryPlayback() {
+	if (!videoContext || !currentVideoUrl.value) {
+		return
+	}
+	try {
+		videoContext.play()
+	} catch {
+		playbackFailed.value = true
+	}
+}
+
+/**
+ * 按章节续播位置 seek。
+ */
+function seekToResumePosition() {
+	if (!videoContext || !currentVideo.value) {
+		return
+	}
+	const seekSeconds = normalizeNumber(currentVideo.value.lastPlayPositionSeconds)
+	if (seekSeconds <= 0) {
+		return
+	}
+	isSeekingPlayback.value = true
+	currentPlaybackSeconds.value = seekSeconds
+	videoContext.seek(seekSeconds)
+	setTimeout(() => {
+		isSeekingPlayback.value = false
+	}, 300)
+}
+
+/**
+ * 记录当前播放片段起点。
+ */
+function startWatchSegment() {
+	if (currentWatchStartedAt.value > 0) {
+		return
+	}
+	currentWatchStartedAt.value = Math.floor(Date.now() / 1000)
+}
+
+/**
+ * 启动播放心跳,保证 H5 环境下持续播放时也能定时落库。
+ */
+function startWatchHeartbeat() {
+	if (watchHeartbeatTimer) {
+		return
+	}
+	watchHeartbeatTimer = setInterval(() => {
+		if (!shouldFlushWatchHeartbeat()) {
+			return
+		}
+		submitCurrentWatchSegment()
+	}, 15000)
+}
+
+/**
+ * 清理播放心跳定时器,避免页面离开后继续上报。
+ */
+function clearWatchHeartbeatTimer() {
+	if (!watchHeartbeatTimer) {
+		return
+	}
+	clearInterval(watchHeartbeatTimer)
+	watchHeartbeatTimer = null
+}
+
+/**
+ * 判断当前是否满足心跳补提条件,避免无效空请求。
+ */
+function shouldFlushWatchHeartbeat() {
+	if (watchSegmentSubmitting.value || !currentWatchStartedAt.value || !currentVideo.value) {
+		return false
+	}
+	if (isSeekingPlayback.value) {
+		return false
+	}
+	return currentPlaybackSeconds.value > lastReportedPlaybackSeconds.value
+}
+
+/**
+ * 提交当前播放片段。
+ */
+async function submitCurrentWatchSegment(forcePositionSeconds?: number) {
+	if (watchSegmentSubmitting.value) {
+		return false
+	}
+	if (!courseId.value || !currentVideo.value) {
+		return false
+	}
+	if (!currentWatchStartedAt.value) {
+		return false
+	}
+	const now = new Date()
+	const startedAt = currentWatchStartedAt.value
+	const durationSeconds = Math.max(Math.floor(Date.now() / 1000) - currentWatchStartedAt.value, 0)
+	currentWatchStartedAt.value = 0
+	if (durationSeconds <= 0 && typeof forcePositionSeconds !== 'number') {
+		return false
+	}
+	const playEnd = now
+	const playStart = new Date(playEnd.getTime() - durationSeconds * 1000)
+	const lastPlayPositionSeconds = typeof forcePositionSeconds === 'number'
+		? Math.max(forcePositionSeconds, 0)
+		: resolveCurrentPositionSeconds()
+	try {
+		watchSegmentSubmitting.value = true
+		const response = await uploadMyHealthCoursePlayRecord(courseId.value, currentVideo.value.videoId, {
+			playStart: formatDateTime(playStart),
+			playEnd: formatDateTime(playEnd),
+			durationSeconds,
+			lastPlayPositionSeconds,
+		})
+		if (response?.code !== 0 || !response.data) {
+			currentWatchStartedAt.value = startedAt
+			return false
+		}
+		updateActiveVideoProgress(
+			normalizePercent(response.data.progressPercent),
+			!!response.data.completed,
+			normalizeNumber(response.data.lastPlayPositionSeconds),
+		)
+		lastReportedPlaybackSeconds.value = Math.max(lastPlayPositionSeconds, 0)
+		if (!response.data.completed && currentPlaybackSeconds.value > 0) {
+			startWatchSegment()
+		}
+		return true
+	} catch {
+		currentWatchStartedAt.value = startedAt
+		return false
+	} finally {
+		watchSegmentSubmitting.value = false
+	}
+}
+
+/**
+ * 估算当前播放位置,供暂停/离开页面时上报续播点。
+ */
+function resolveCurrentPositionSeconds() {
+	if (currentVideo.value?.completed) {
+		return 0
+	}
+	if (currentPlaybackSeconds.value > 0) {
+		return normalizePlayPosition(currentPlaybackSeconds.value)
+	}
+	return normalizePlayPosition(currentVideo.value?.lastPlayPositionSeconds)
+}
+
+/**
+ * 更新当前章节的本地进度,避免页面等待下一次刷新。
+ */
+function updateActiveVideoProgress(progressPercent: number, completed: boolean, lastPlayPositionSeconds: number) {
+	const nextVideos = detail.value.videos.map((item) => {
+		if (item.videoId !== activeVideoId.value) {
+			return item
+		}
+		return {
+			...item,
+			progressPercent,
+			completed,
+			lastPlayPositionSeconds,
+		}
+	})
+	detail.value = {
+		...detail.value,
+		videos: nextVideos,
+	}
+	currentPlaybackSeconds.value = completed ? 0 : normalizePlayPosition(lastPlayPositionSeconds)
+	lastReportedPlaybackSeconds.value = completed ? 0 : normalizePlayPosition(lastPlayPositionSeconds)
+}
+
+/**
+ * 切换播放章节,先提交当前片段再切换,避免丢进度。
+ */
+async function switchVideo(videoId: number) {
+	if (!videoId || videoId === activeVideoId.value) {
+		return
+	}
+	clearWatchHeartbeatTimer()
+	await submitCurrentWatchSegment()
+	activeVideoId.value = videoId
+	resetPlaybackState()
+	initVideoContext()
+}
+
+/**
+ * 处理章节下载。
+ */
+function handleVideoDownload(item: LessonViewItem) {
+	if (!item.downloadable) {
+		uni.showToast({
+			title: '当前章节不可下载',
+			icon: 'none',
+		})
+		return
+	}
+	if (!item.videoUrl) {
+		uni.showToast({
+			title: '视频地址不存在',
+			icon: 'none',
+		})
+		return
+	}
+	// H5 端直接打开资源地址,避免浏览器环境下 `downloadFile` 因跨域策略导致下载失败。
+	// #ifdef H5
+	window.open(item.videoUrl, '_blank')
+	uni.showToast({
+		title: '已打开下载地址',
+		icon: 'none',
+	})
+	return
+	// #endif
+	const currentUserId = normalizeNumber(resolveCurrentUserId())
+	if (!currentUserId) {
+		uni.showToast({
+			title: '请先登录',
+			icon: 'none',
+		})
+		return
+	}
+	if (hasDownloadedTask(listCurrentDownloadTasks(), item.videoId)) {
+		uni.showToast({
+			title: '当前章节正在下载中',
+			icon: 'none',
+		})
+		return
+	}
+	const downloadTaskId = buildDownloadTaskId(item.videoId)
+	const initialTask = createDownloadTaskPayload(downloadTaskId, item)
+	upsertDownloadTask(currentUserId, initialTask)
+	uni.showLoading({
+		title: '下载中...',
+		mask: true,
+	})
+	const downloadTask = uni.downloadFile({
+		url: item.videoUrl,
+		success: async (response) => {
+			if (response.statusCode !== 200 || !response.tempFilePath) {
+				clearDownloadTask(currentUserId, downloadTaskId)
+				uni.showToast({
+					title: '下载失败',
+					icon: 'none',
+				})
+				return
+			}
+			// 先保存到持久化目录,再同步下载记录,避免临时文件失效后“我的下载”里只有记录没有文件。
+			uni.saveFile({
+				tempFilePath: response.tempFilePath,
+				success: async () => {
+					clearDownloadTask(currentUserId, downloadTaskId)
+					try {
+						const reportResponse = await reportHealthCourseDownload({
+							courseId: detail.value.courseId,
+							courseVideoId: item.videoId,
+						})
+						if (reportResponse?.code !== 0) {
+							return
+						}
+						uni.showToast({
+							title: '下载完成',
+							icon: 'none',
+						})
+					} catch {
+						uni.showToast({
+							title: '下载完成,记录同步失败',
+							icon: 'none',
+						})
+					}
+				},
+				fail: () => {
+					clearDownloadTask(currentUserId, downloadTaskId)
+					uni.showToast({
+						title: '文件已下载到临时目录',
+						icon: 'none',
+					})
+				},
+			})
+		},
+		fail: () => {
+			clearDownloadTask(currentUserId, downloadTaskId)
+			uni.showToast({
+				title: '下载失败',
+				icon: 'none',
+			})
+		},
+		complete: () => {
+			uni.hideLoading()
+		},
+	})
+	downloadTask.onProgressUpdate((progressEvent) => {
+		upsertDownloadTask(currentUserId, {
+			...initialTask,
+			progress: progressEvent.progress,
+			updatedAt: new Date().toISOString(),
+		})
+	})
+}
+
+/**
+ * 拨打系统配置的客服电话,未配置时给出提示。
+ */
+function handleConsult() {
+	const servicePhone = detail.value.servicePhone.trim()
+	if (!servicePhone) {
+		uni.showToast({
+			title: '暂未配置客服电话',
+			icon: 'none',
+		})
+		return
+	}
+	uni.makePhoneCall({
+		phoneNumber: servicePhone,
+	})
+}
+
+/**
+ * 底部下载按钮直接下载当前正在播放的章节。
+ */
+function handleCurrentVideoDownload() {
+	if (!currentVideo.value) {
+		uni.showToast({
+			title: '暂无可下载章节',
+			icon: 'none',
+		})
+		return
+	}
+	handleVideoDownload({
+		videoId: currentVideo.value.videoId,
+		title: currentVideo.value.chapterName,
+		duration: currentVideo.value.durationText,
+		progress: normalizePercent(currentVideo.value.progressPercent),
+		isDownloaded: hasDownloadedTask(listCurrentDownloadTasks(), currentVideo.value.videoId),
+		isActive: true,
+		downloadable: !!currentVideo.value.downloadable,
+		videoUrl: currentVideo.value.videoUrl,
+		durationSeconds: currentVideo.value.durationSeconds,
+	})
+}
+
+/**
+ * 构建下载任务快照。
+ */
+function createDownloadTaskPayload(clientTaskId: string, item: LessonViewItem): LocalDownloadTask {
+	return {
+		clientTaskId,
+		courseId: detail.value.courseId,
+		courseVideoId: item.videoId,
+		title: detail.value.title || '养生课程',
+		subtitle: item.title,
+		coverImage: detail.value.coverImage,
+		durationSeconds: item.durationSeconds,
+		durationText: item.duration,
+		videoUrl: item.videoUrl,
+		progress: 0,
+		status: 'downloading',
+		updatedAt: new Date().toISOString(),
+	}
+}
+
+/**
+ * 构建下载任务唯一标识。
+ */
+function buildDownloadTaskId(videoId: number) {
+	return `my-course-${detail.value.courseId}-${videoId}`
+}
+
+/**
+ * 读取当前用户的下载任务列表。
+ */
+function listCurrentDownloadTasks(): LocalDownloadTask[] {
+	const currentUserId = normalizeNumber(resolveCurrentUserId())
+	if (!currentUserId) {
+		return []
+	}
+	return listDownloadTasks(currentUserId) as LocalDownloadTask[]
+}
+
+/**
+ * 判断章节是否已在下载列表中。
+ */
+function hasDownloadedTask(tasks: LocalDownloadTask[], videoId: number) {
+	return tasks.some((item) => item.courseVideoId === videoId)
+}
+
+/**
+ * 获取当前登录用户主键。
+ */
+function resolveCurrentUserId() {
+	const userInfo = uni.getStorageSync('appUserInfo')
+	if (!userInfo || typeof userInfo !== 'object') {
+		return 0
+	}
+	return (userInfo.id || userInfo.userId || 0) as number
+}
+
+/**
+ * 跳转到课程介绍页。
+ */
+function goCourseIntro() {
+	if (!detail.value.courseId) {
+		return
+	}
+	uni.navigateTo({
+		url: `/pages/courseDetail/courseDetail?id=${detail.value.courseId}`,
+	})
+}
+
+/**
+ * 返回上一页,优先完成播放进度上报,避免直接返回导致续播点丢失。
+ */
+async function goBack() {
+	if (backSubmitting.value) {
+		return
+	}
+	backSubmitting.value = true
+	try {
+		clearWatchHeartbeatTimer()
+		if (currentWatchStartedAt.value > 0) {
+			uni.showLoading({
+				title: '保存进度中...',
+				mask: true,
+			})
+			await submitCurrentWatchSegment()
+			uni.hideLoading()
+		}
+		uni.navigateBack()
+	} finally {
+		backSubmitting.value = false
+	}
+}
+
+/**
+ * 格式化日期时间,兼容后端 LocalDateTime 解析。
+ */
+function formatDateTime(date: Date) {
+	const year = String(date.getFullYear())
+	const month = String(date.getMonth() + 1).padStart(2, '0')
+	const day = String(date.getDate()).padStart(2, '0')
+	const hour = String(date.getHours()).padStart(2, '0')
+	const minute = String(date.getMinutes()).padStart(2, '0')
+	const second = String(date.getSeconds()).padStart(2, '0')
+	return `${year}-${month}-${day} ${hour}:${minute}:${second}`
+}
+
+/**
+ * 归一化数字。
+ */
+function normalizeNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return 0
+	}
+	return numberValue
+}
+
+/**
+ * 归一化文本。
+ */
+function normalizeText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
+
+/**
+ * 归一化百分比。
+ */
+function normalizePercent(value: unknown) {
+	const percent = Math.round(Number(value || 0))
+	if (!Number.isFinite(percent)) {
+		return 0
+	}
+	if (percent < 0) {
+		return 0
+	}
+	if (percent > 100) {
+		return 100
+	}
+	return percent
+}
+
+/**
+ * 按当前视频时长归一化播放位置,避免越界。
+ */
+function normalizePlayPosition(value: unknown) {
+	const seconds = Math.max(Math.floor(Number(value || 0)), 0)
+	const maxDuration = mediaDurationSeconds.value > 0
+		? mediaDurationSeconds.value
+		: normalizeNumber(currentVideo.value?.durationSeconds)
+	if (maxDuration <= 0) {
+		return seconds
+	}
+	if (seconds > maxDuration) {
+		return maxDuration
+	}
+	return seconds
+}
+
+/**
+ * H5 页面进入后台或关闭时补提当前片段,减少浏览器生命周期导致的丢进度。
+ */
+function handleH5VisibilityChange() {
+	if (typeof document === 'undefined' || !document.hidden) {
+		return
+	}
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment()
+}
+
+/**
+ * H5 页面关闭或刷新前补提当前播放片段。
+ */
+function handleH5PageHide() {
+	clearWatchHeartbeatTimer()
+	submitCurrentWatchSegment()
+}
+
+/**
+ * 绑定 H5 浏览器生命周期监听,兼容返回、切后台、刷新等场景。
+ */
+function bindH5PlaybackLifecycle() {
+	if (!isH5 || typeof window === 'undefined' || typeof document === 'undefined') {
+		return
+	}
+	unbindH5PlaybackLifecycle()
+	document.addEventListener('visibilitychange', handleH5VisibilityChange)
+	window.addEventListener('pagehide', handleH5PageHide)
+}
+
+/**
+ * 移除 H5 浏览器生命周期监听,避免重复绑定。
+ */
+function unbindH5PlaybackLifecycle() {
+	if (!isH5 || typeof window === 'undefined' || typeof document === 'undefined') {
+		return
+	}
+	document.removeEventListener('visibilitychange', handleH5VisibilityChange)
+	window.removeEventListener('pagehide', handleH5PageHide)
+}
 </script>
 
 <style lang="scss" scoped>
@@ -122,7 +992,6 @@
 		background-color: #FFFFFF;
 	}
 
-	/* 自定义导航栏 */
 	.custom-navbar {
 		width: 100%;
 		background-color: #FFFFFF;
@@ -153,7 +1022,6 @@
 		width: 40rpx;
 	}
 
-	/* 视频区域 */
 	.video-section {
 		width: 100%;
 		height: 420rpx;
@@ -161,59 +1029,30 @@
 		background-color: #000;
 	}
 
+	.video-player-wrapper,
+	.video-player,
 	.video-poster {
 		width: 100%;
 		height: 100%;
 	}
 
-	.play-btn-wrap {
+	.video-retry-mask {
 		position: absolute;
-		top: 50%;
-		left: 50%;
-		transform: translate(-50%, -50%);
-		width: 100rpx;
-		height: 100rpx;
-		background: rgba(255, 255, 255, 0.3);
-		border-radius: 50%;
+		inset: 0;
 		display: flex;
+		align-items: center;
 		justify-content: center;
-		align-items: center;
+		background: rgba(0, 0, 0, 0.32);
 	}
 
-	.play-icon {
-		width: 40rpx;
-		height: 40rpx;
+	.video-retry-text {
+		font-size: 28rpx;
+		color: #FFFFFF;
+		padding: 20rpx 32rpx;
+		background: rgba(0, 0, 0, 0.55);
+		border-radius: 999rpx;
 	}
 
-	.video-controls {
-		position: absolute;
-		bottom: 0;
-		left: 0;
-		width: 100%;
-		height: 60rpx;
-		background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.5) 100%);
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		padding: 0 24rpx;
-		box-sizing: border-box;
-	}
-
-	.controls-left, .controls-right {
-		display: flex;
-		align-items: center;
-	}
-
-	.control-icon {
-		width: 32rpx;
-		height: 32rpx;
-		background-color: #fff; /* 占位色块,实际应为图标 */
-		margin: 0 10rpx;
-		border-radius: 4rpx;
-		opacity: 0.8;
-	}
-
-	/* 列表头部 */
 	.list-header {
 		padding: 32rpx;
 		display: flex;
@@ -233,10 +1072,19 @@
 		color: #86909C;
 	}
 
-	/* 课程列表 */
 	.lesson-list {
 		flex: 1;
 		height: 0;
+	}
+
+	.empty-box {
+		padding: 180rpx 0;
+		text-align: center;
+	}
+
+	.empty-text {
+		font-size: 28rpx;
+		color: #999999;
 	}
 
 	.lesson-item {
@@ -282,7 +1130,7 @@
 		font-size: 28rpx;
 		color: #333333;
 		font-weight: 500;
-		
+
 		&.active-text {
 			color: #D32E29;
 		}
@@ -308,7 +1156,6 @@
 		height: 30rpx;
 	}
 
-	/* 进度条 */
 	.progress-wrap {
 		width: 100%;
 		display: flex;
@@ -319,67 +1166,63 @@
 	.progress-bar {
 		flex: 1;
 		height: 8rpx;
-		background-color: #F0F0F0;
-		border-radius: 4rpx;
+		border-radius: 999rpx;
+		background: #F2F3F5;
 		overflow: hidden;
-		margin-right: 16rpx;
 	}
 
 	.progress-inner {
 		height: 100%;
-		background-color: #D32E29;
-		border-radius: 4rpx;
+		border-radius: 999rpx;
+		background: linear-gradient(90deg, #D32E29 0%, #F07A74 100%);
 	}
 
 	.progress-text {
-		font-size: 24rpx;
-		color: #86909C;
-		width: 60rpx;
-		text-align: right;
+		margin-left: 12rpx;
+		font-size: 22rpx;
+		color: #D32E29;
 	}
 
-	/* 底部操作栏 */
 	.bottom-bar {
-		width: 100%;
-		height: 120rpx;
+		height: 112rpx;
+		border-top: 1px solid #F5F5F5;
 		background-color: #FFFFFF;
 		display: flex;
 		align-items: center;
 		padding: 0 32rpx;
 		box-sizing: border-box;
-		border-top: 1px solid #E5E6EB;
-		padding-bottom: env(safe-area-inset-bottom);
 	}
 
 	.action-btn {
+		width: 96rpx;
 		display: flex;
 		flex-direction: column;
 		align-items: center;
 		justify-content: center;
-		margin-right: 48rpx;
+		margin-right: 24rpx;
 	}
 
 	.action-icon {
-		width: 44rpx;
-		height: 44rpx;
-		margin-bottom: 6rpx;
+		width: 36rpx;
+		height: 36rpx;
+		margin-bottom: 8rpx;
 	}
 
 	.action-text {
-		font-size: 20rpx;
-		color: #333333;
+		font-size: 22rpx;
+		color: #666666;
 	}
 
 	.main-btn {
 		flex: 1;
 		height: 80rpx;
-		background-color: #D32E29;
+		border-radius: 999rpx;
+		background: linear-gradient(90deg, #D32E29 0%, #F07A74 100%);
 		color: #FFFFFF;
 		font-size: 30rpx;
-		border-radius: 12rpx;
-		display: flex;
-		justify-content: center;
-		align-items: center;
 		font-weight: 500;
+		display: flex;
+		align-items: center;
+		justify-content: center;
 	}
-</style>
\ No newline at end of file
+</style>
diff --git a/traditionalApp/pages/myCourseDetail/service.js b/traditionalApp/pages/myCourseDetail/service.js
new file mode 100644
index 0000000..e140b6a
--- /dev/null
+++ b/traditionalApp/pages/myCourseDetail/service.js
@@ -0,0 +1,15 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取 APP 我的课程详情。
+ */
+export const getMyHealthCourseDetail = (courseId) => {
+	return request.get(`/api/app/my-health-courses/${courseId}`, {}, false, false)
+}
+
+/**
+ * 上报 APP 我的课程播放记录。
+ */
+export const uploadMyHealthCoursePlayRecord = (courseId, videoId, data) => {
+	return request.post(`/api/app/my-health-courses/${courseId}/videos/${videoId}/play-records`, data, false, false)
+}
diff --git a/traditionalApp/pages/myCourses/myCourses.vue b/traditionalApp/pages/myCourses/myCourses.vue
index b4e286f..94b04a2 100644
--- a/traditionalApp/pages/myCourses/myCourses.vue
+++ b/traditionalApp/pages/myCourses/myCourses.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -10,18 +9,19 @@
 		</view>
 
 		<scroll-view scroll-y class="courses-list">
-			<block v-for="(item, index) in courseList" :key="item.id">
+			<view v-if="!loading && courseList.length === 0" class="empty-box">
+				<text class="empty-text">暂无课程</text>
+			</view>
+			<block v-for="(item, index) in courseList" :key="item.courseId">
 				<view class="list-item-wrapper">
 					<view class="swipe-wrap">
-						<u-swipe-action :options="swipeOptions" :index="index" @click="handleDelete(index)">
-							<view class="course-item-container" @tap="handleItemClick(index)">
+						<u-swipe-action :options="resolveSwipeOptions(item)" :index="index" @click="handleDelete(index)">
+							<view class="course-item-container" @tap="handleItemClick(item)">
 								<view class="course-item">
-									<!-- 图片 -->
 									<view class="img-wrap">
-										<image class="course-img" :src="item.image" mode="aspectFill"></image>
+										<image class="course-img" :src="resolveCoverImage(item.coverImage)" mode="aspectFill"></image>
 									</view>
 
-									<!-- 课程信息 -->
 									<view class="course-info">
 										<view class="title-wrap">
 											<view class="tag-list">
@@ -36,9 +36,7 @@
 												<text class="study-current">{{ item.currentLessons }}</text>
 												<text class="study-total">/{{ item.totalLessons }}</text>
 											</view>
-											<!-- 环形进度条展示区 (模拟图中的环形) -->
 											<view class="circle-progress-wrap">
-												<!-- 这里由于没有具体环形进度组件,用一个简单的带边框圆形模拟,中间放百分比 -->
 												<view class="circle-border">
 													<text class="circle-text">{{ item.progress }}%</text>
 												</view>
@@ -56,43 +54,90 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { getMyHealthCourses, removeMyHealthCourse } from './service'
 
-	const statusBarHeight = ref(20);
+interface MyHealthCourseItem {
+	courseId: number
+	title: string
+	coverImage: string
+	hasHealthTag: boolean
+	currentLessons: number
+	totalLessons: number
+	progress: number
+	removable: boolean
+}
 
-	// 模拟数据
-	const courseList = ref([
-		{
-			id: 1,
-			title: '拍八虚新手入门版',
-			image: '/static/image/home/doctor.png', // 示例图
-			hasHealthTag: true,
-			currentLessons: 25,
-			totalLessons: 50,
-			progress: 50
-		},
-		{
-			id: 2,
-			title: '拍八虚新手入门版',
-			image: '/static/image/home/doctor.png',
-			hasHealthTag: true,
-			currentLessons: 25,
-			totalLessons: 50,
-			progress: 50
-		},
-		{
-			id: 3,
-			title: '拍八虚新手入门版',
-			image: '/static/image/home/doctor.png',
-			hasHealthTag: true,
-			currentLessons: 25,
-			totalLessons: 50,
-			progress: 50
+const DEFAULT_COURSE_IMAGE = '/static/image/home/class.png'
+
+const statusBarHeight = ref(20)
+const loading = ref(false)
+const courseList = ref<MyHealthCourseItem[]>([])
+
+/**
+ * 页面加载时初始化状态栏高度。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+})
+
+/**
+ * 页面显示时刷新我的课程列表。
+ */
+onShow(() => {
+	loadMyHealthCourses()
+})
+
+/**
+ * 拉取当前登录用户的我的课程列表。
+ */
+async function loadMyHealthCourses() {
+	loading.value = true
+	try {
+		const response = await getMyHealthCourses()
+		if (response?.code !== 0) {
+			return
 		}
-	]);
+		courseList.value = normalizeCourseList(response?.data)
+	} catch {
+		// 请求层已统一处理异常提示。
+	} finally {
+		loading.value = false
+	}
+}
 
-	// u-swipe-action 右侧滑出选项
-	const swipeOptions = ref([
+/**
+ * 将接口数据归一化为页面渲染结构。
+ */
+function normalizeCourseList(value: unknown): MyHealthCourseItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const course = (item && typeof item === 'object' ? item : {}) as Record<string, unknown>
+		return {
+			courseId: normalizeNumber(course.courseId),
+			title: normalizeText(course.title) || '养生课程',
+			coverImage: normalizeText(course.coverImage),
+			hasHealthTag: !!course.hasHealthTag,
+			currentLessons: normalizeNumber(course.completedLessons),
+			totalLessons: normalizeNumber(course.totalLessons),
+			progress: normalizePercent(course.progressPercent),
+			removable: !!course.removable,
+		}
+	}).filter((item) => item.courseId > 0)
+}
+
+/**
+ * 根据课程是否允许移除返回滑动按钮配置。
+ */
+function resolveSwipeOptions(item: MyHealthCourseItem) {
+	if (!item.removable) {
+		return []
+	}
+	return [
 		{
 			text: '删除',
 			style: {
@@ -105,41 +150,114 @@
 				display: 'flex',
 				justifyContent: 'center',
 				alignItems: 'center',
-				margin: '0 30rpx'
+				margin: '0 30rpx',
+			},
+		},
+	]
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 跳转到我的课程详情页。
+ */
+function handleItemClick(item: MyHealthCourseItem) {
+	if (!item.courseId) {
+		return
+	}
+	uni.navigateTo({
+		url: `/pages/myCourseDetail/myCourseDetail?courseId=${item.courseId}`,
+	})
+}
+
+/**
+ * 删除会员加入的课程,已购课程不允许删除。
+ */
+function handleDelete(index: number) {
+	const item = courseList.value[index]
+	if (!item) {
+		return
+	}
+	if (!item.removable) {
+		uni.showToast({
+			title: '已购买课程不可删除',
+			icon: 'none',
+		})
+		return
+	}
+	uni.showModal({
+		title: '提示',
+		content: '确认删除该课程记录?',
+		success: async (res) => {
+			if (!res.confirm) {
+				return
 			}
-		}
-	]);
-
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
-
-	const goBack = () => {
-		uni.navigateBack();
-	};
-
-	const handleItemClick = (index: number) => {
-		uni.navigateTo({
-			url: '/pages/myCourseDetail/myCourseDetail'
-		});
-	};
-
-	const handleDelete = (index: number) => {
-		uni.showModal({
-			title: '提示',
-			content: '确认删除该课程记录?',
-			success: (res) => {
-				if (res.confirm) {
-					courseList.value.splice(index, 1);
-					uni.showToast({
-						title: '已删除',
-						icon: 'none'
-					});
+			try {
+				const response = await removeMyHealthCourse(item.courseId)
+				if (response?.code !== 0) {
+					return
 				}
+				courseList.value.splice(index, 1)
+				uni.showToast({
+					title: '已删除',
+					icon: 'none',
+				})
+			} catch {
+				// 请求层已统一处理异常提示。
 			}
-		});
-	};
+		},
+	})
+}
+
+/**
+ * 解析课程封面,避免空图导致布局异常。
+ */
+function resolveCoverImage(value?: string) {
+	return normalizeText(value) || DEFAULT_COURSE_IMAGE
+}
+
+/**
+ * 将数字归一化。
+ */
+function normalizeNumber(value: unknown) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return 0
+	}
+	return numberValue
+}
+
+/**
+ * 将文本归一化为空字符串。
+ */
+function normalizeText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
+
+/**
+ * 归一化百分比,避免出现无效值。
+ */
+function normalizePercent(value: unknown) {
+	const percent = Math.round(Number(value || 0))
+	if (!Number.isFinite(percent)) {
+		return 0
+	}
+	if (percent < 0) {
+		return 0
+	}
+	if (percent > 100) {
+		return 100
+	}
+	return percent
+}
 </script>
 
 <style lang="scss" scoped>
@@ -155,7 +273,6 @@
 		flex-direction: column;
 	}
 
-	/* 自定义导航栏 */
 	.custom-navbar {
 		width: 100%;
 		background-color: #FFFFFF;
@@ -189,13 +306,22 @@
 		width: 40rpx;
 	}
 
-	/* 课程列表 */
 	.courses-list {
 		flex: 1;
 		height: 0;
 		padding: 24rpx 32rpx;
 		box-sizing: border-box;
 		width: 100%;
+	}
+
+	.empty-box {
+		padding: 200rpx 0;
+		text-align: center;
+	}
+
+	.empty-text {
+		font-size: 28rpx;
+		color: #999999;
 	}
 
 	.list-item-wrapper {
@@ -211,7 +337,6 @@
 		transition: all 0.3s;
 	}
 
-	// 修改 u-swipe-action 的样式适应圆形删除按钮设计
 	:deep(.u-swipe-action) {
 		background-color: transparent !important;
 	}
@@ -231,7 +356,7 @@
 	}
 
 	:deep(.u-btn-text) {
-		display: none; // 隐藏文字
+		display: none;
 	}
 
 	:deep(.u-swipe-del::after) {
@@ -258,122 +383,104 @@
 		width: 100%;
 		height: 200rpx;
 		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+		border-radius: 16rpx;
 		padding: 24rpx;
 		box-sizing: border-box;
 		display: flex;
 		align-items: center;
-		border-radius: 24rpx;
 	}
 
 	.img-wrap {
-		position: relative;
 		width: 152rpx;
-		height: 152rpx;
-		border-radius: 16rpx;
-		margin-right: 24rpx;
+		height: 112rpx;
+		border-radius: 12rpx;
+		overflow: hidden;
 		flex-shrink: 0;
-		background-color: #BDBDBD;
-		overflow: visible;
+		margin-right: 24rpx;
 	}
 
 	.course-img {
 		width: 100%;
 		height: 100%;
-		border-radius: 16rpx;
 	}
 
 	.course-info {
 		flex: 1;
-		height: 152rpx;
+		min-width: 0;
 		display: flex;
 		flex-direction: column;
 		justify-content: space-between;
-		min-width: 0;
+		height: 112rpx;
 	}
 
 	.title-wrap {
 		display: flex;
-		align-items: center;
-		flex-wrap: wrap;
+		flex-direction: column;
 	}
 
 	.tag-list {
 		display: flex;
 		align-items: center;
+		margin-bottom: 12rpx;
 	}
 
 	.tag {
-		font-size: 20rpx;
-		padding: 2rpx 10rpx;
+		padding: 4rpx 12rpx;
 		border-radius: 8rpx;
-		margin-right: 12rpx;
-		color: #FFFFFF;
+		font-size: 20rpx;
+		line-height: 28rpx;
 	}
 
 	.red-tag {
-		background-color: #D32E29;
+		background: rgba(211, 46, 41, 0.1);
+		color: #D32E29;
 	}
 
 	.course-title {
-		font-size: 32rpx;
+		font-size: 28rpx;
+		line-height: 40rpx;
+		font-weight: 500;
 		color: #1A1A1A;
-		font-weight: bold;
-		flex: 1;
-		white-space: nowrap;
 		overflow: hidden;
 		text-overflow: ellipsis;
+		white-space: nowrap;
 	}
 
 	.course-bottom {
 		display: flex;
+		align-items: center;
 		justify-content: space-between;
-		align-items: flex-end;
 	}
 
 	.study-progress {
 		font-size: 24rpx;
 		color: #86909C;
-		margin-bottom: 4rpx;
+	}
 
-		.study-label {
-			color: #86909C;
-		}
-
-		.study-current {
-			color: #D32E29;
-			font-weight: bold;
-		}
-
-		.study-total {
-			color: #86909C;
-		}
+	.study-current {
+		color: #D32E29;
 	}
 
 	.circle-progress-wrap {
-		width: 80rpx;
-		height: 80rpx;
 		display: flex;
-		justify-content: center;
 		align-items: center;
+		justify-content: center;
 	}
 
 	.circle-border {
-		width: 80rpx;
-		height: 80rpx;
+		width: 70rpx;
+		height: 70rpx;
 		border-radius: 50%;
-		border: 12rpx solid #E6D4CB; /* 模拟底色圈 */
-		border-top-color: #D32E29; /* 模拟进度颜色 */
-		border-right-color: #D32E29;
+		border: 4rpx solid #D32E29;
 		display: flex;
-		justify-content: center;
 		align-items: center;
-		transform: rotate(-45deg); /* 调整红色进度条的起始角度以更像图片 */
+		justify-content: center;
+		background-color: #FFFFFF;
 	}
 
 	.circle-text {
 		font-size: 20rpx;
-		color: #1A1A1A;
-		font-weight: bold;
-		transform: rotate(45deg); /* 把文字转正 */
+		color: #D32E29;
+		font-weight: 500;
 	}
-</style>
\ No newline at end of file
+</style>
diff --git a/traditionalApp/pages/myCourses/service.js b/traditionalApp/pages/myCourses/service.js
new file mode 100644
index 0000000..573dd32
--- /dev/null
+++ b/traditionalApp/pages/myCourses/service.js
@@ -0,0 +1,15 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取 APP 我的课程列表。
+ */
+export const getMyHealthCourses = () => {
+	return request.get('/api/app/my-health-courses', {}, false, false)
+}
+
+/**
+ * 删除 APP 我的课程中的会员加课课程。
+ */
+export const removeMyHealthCourse = (courseId) => {
+	return request.delete(`/api/app/my-health-courses/${courseId}`, {}, false, false)
+}
diff --git a/traditionalApp/pages/point/point.vue b/traditionalApp/pages/point/point.vue
index aec2bd7..276928f 100644
--- a/traditionalApp/pages/point/point.vue
+++ b/traditionalApp/pages/point/point.vue
@@ -2,7 +2,7 @@
 	<view class="container">
 		<!-- 顶部背景图 -->
 		<view class="top-bg">
-			<image src="https://mxcd.zhinenganguan.com/file/2026-03-30/5d3dd485-5af1-4913-ba22-c27f668c4660.png" mode="widthFix" class="bg-img"></image>
+			<image :src="topBackgroundImage" mode="widthFix" class="bg-img"></image>
 		</view>
 
 		<!-- 自定义导航栏 -->
@@ -25,33 +25,33 @@
 				
 				<view class="point-main">
 					<view class="point-number-box">
-						<text class="point-current">15</text>
-						<text class="point-total">/150</text>
+						<text class="point-current">{{ overview.pointsBalance }}</text>
+						<text class="point-total">/{{ overview.requiredPoints }}</text>
 					</view>
 					<view class="point-detail-btn" @tap="goPointDetail">积分明细</view>
 				</view>
 
 				<view class="progress-container">
 					<view class="progress-bar">
-						<view class="progress-inner" :style="{ width: '10%' }"></view>
+						<view class="progress-inner" :style="{ width: `${overview.progressPercent}%` }"></view>
 					</view>
-					<text class="progress-text">40%</text>
+					<text class="progress-text">{{ overview.progressPercent }}%</text>
 				</view>
 
 				<view class="point-date">
-					<text>本期时间  2026.01.01-2026.01.01</text>
+					<text>本期时间 {{ displayActivityPeriod }}</text>
 				</view>
 			</view>
 
 			<!-- 兑换提示 -->
 			<view class="exchange-card">
-				<image class="exchange-img" src="/static/image/point/flower.png" mode="aspectFill"></image>
+				<image class="exchange-img" :src="displayGiftCover" mode="aspectFill"></image>
 				<view class="exchange-info">
 					<view class="exchange-title">
-						满<text class="highlight">150</text>积分即可兑换
+						满<text class="highlight">{{ overview.requiredPoints }}</text>积分即可兑换
 					</view>
 					<view class="exchange-desc">礼品将在活动接收发放至您绑定的诊所</view>
-					<view class="exchange-desc">还差<text class="highlight">132</text>积分即可领取</view>
+					<view class="exchange-desc">还差<text class="highlight">{{ overview.pointsShortfall }}</text>积分即可领取</view>
 				</view>
 			</view>
 
@@ -78,10 +78,10 @@
 						<image class="clinic-icon" src="/static/image/point/home.png" mode="aspectFit"></image>
 					</view>
 					<view class="address-info">
-						<view class="clinic-name">XXXXXXXXX诊所名称</view>
+						<view class="clinic-name">{{ displayClinicName }}</view>
 						<view class="clinic-address">
 							<image class="location-icon" src="/static/image/point/location.png" mode="aspectFit"></image>
-							<text>四川省成都市锦晖西二街399号</text>
+							<text>{{ displayClinicAddress }}</text>
 						</view>
 					</view>
 				</view>
@@ -91,19 +91,175 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+	import { computed, ref } from 'vue';
+	import { onLoad, onShow } from '@dcloudio/uni-app';
+	import { getPointsOverview } from './service';
+
+	interface PointsOverview {
+		pointsBalance: number
+		requiredPoints: number
+		progressPercent: number
+		pointsShortfall: number
+		activityStartAt: string
+		activityEndAt: string
+		giftCover: string
+		giftName: string
+		clinicName: string
+		clinicAddress: string
+	}
+
+	const DEFAULT_TOP_BACKGROUND = 'https://mxcd.zhinenganguan.com/file/2026-03-30/5d3dd485-5af1-4913-ba22-c27f668c4660.png';
+	const DEFAULT_GIFT_IMAGE = '/static/image/point/flower.png';
 
 	const statusBarHeight = ref(20);
+	const overview = ref<PointsOverview>(createDefaultOverview());
 
-	onMounted(() => {
+	const topBackgroundImage = computed(() => DEFAULT_TOP_BACKGROUND);
+	const displayGiftCover = computed(() => overview.value.giftCover || DEFAULT_GIFT_IMAGE);
+	const displayActivityPeriod = computed(() => {
+		const startAt = formatDateLabel(overview.value.activityStartAt);
+		const endAt = formatDateLabel(overview.value.activityEndAt);
+		if (!startAt && !endAt) {
+			return '--'
+		}
+		if (!startAt) {
+			return `-- - ${endAt}`
+		}
+		if (!endAt) {
+			return `${startAt} - --`
+		}
+		return `${startAt}-${endAt}`
+	});
+	const displayClinicName = computed(() => overview.value.clinicName || '暂未绑定诊所');
+	const displayClinicAddress = computed(() => overview.value.clinicAddress || '暂未配置诊所地址');
+
+	/**
+	 * 创建我的积分页默认数据,避免首屏空对象访问。
+	 */
+	function createDefaultOverview(): PointsOverview {
+		return {
+			pointsBalance: 0,
+			requiredPoints: 0,
+			progressPercent: 0,
+			pointsShortfall: 0,
+			activityStartAt: '',
+			activityEndAt: '',
+			giftCover: '',
+			giftName: '',
+			clinicName: '',
+			clinicAddress: '',
+		}
+	}
+
+	/**
+	 * 页面加载时初始化状态栏高度。
+	 */
+	onLoad(() => {
 		const systemInfo = uni.getSystemInfoSync();
 		statusBarHeight.value = systemInfo.statusBarHeight || 20;
 	});
 
+	/**
+	 * 页面显示时刷新积分概览,保证积分和活动配置实时同步。
+	 */
+	onShow(() => {
+		loadPointsOverview();
+	});
+
+	/**
+	 * 拉取当前登录用户的积分页面聚合数据。
+	 */
+	async function loadPointsOverview() {
+		try {
+			const response = await getPointsOverview();
+			if (response?.code !== 0 || !response.data) {
+				return;
+			}
+			overview.value = normalizeOverview(response.data);
+		} catch {
+			// 请求层已统一处理异常提示。
+		}
+	}
+
+	/**
+	 * 将接口数据归一化为页面可直接渲染的结构。
+	 */
+	function normalizeOverview(value: unknown): PointsOverview {
+		const source = (value && typeof value === 'object' ? value : {}) as Record<string, unknown>;
+		return {
+			pointsBalance: normalizeNumber(source.pointsBalance),
+			requiredPoints: normalizeNumber(source.requiredPoints),
+			progressPercent: normalizePercent(source.progressPercent),
+			pointsShortfall: normalizeNumber(source.pointsShortfall),
+			activityStartAt: normalizeText(source.activityStartAt),
+			activityEndAt: normalizeText(source.activityEndAt),
+			giftCover: normalizeText(source.giftCover),
+			giftName: normalizeText(source.giftName),
+			clinicName: normalizeText(source.clinicName),
+			clinicAddress: normalizeText(source.clinicAddress),
+		};
+	}
+
+	/**
+	 * 将时间字符串格式化为页面使用的 yyyy.MM.dd 形式。
+	 */
+	function formatDateLabel(value: string) {
+		const text = normalizeText(value);
+		if (!text) {
+			return '';
+		}
+		const datePart = text.split(' ')[0] || '';
+		return datePart.replace(/-/g, '.');
+	}
+
+	/**
+	 * 归一化数字字段。
+	 */
+	function normalizeNumber(value: unknown) {
+		const numberValue = Number(value);
+		if (!Number.isFinite(numberValue)) {
+			return 0;
+		}
+		return numberValue;
+	}
+
+	/**
+	 * 归一化百分比,避免出现越界显示。
+	 */
+	function normalizePercent(value: unknown) {
+		const percent = Math.round(Number(value || 0));
+		if (!Number.isFinite(percent)) {
+			return 0;
+		}
+		if (percent < 0) {
+			return 0;
+		}
+		if (percent > 100) {
+			return 100;
+		}
+		return percent;
+	}
+
+	/**
+	 * 归一化文本,避免模板中出现 null 或 undefined。
+	 */
+	function normalizeText(value: unknown) {
+		if (typeof value !== 'string') {
+			return '';
+		}
+		return value.trim();
+	}
+
+	/**
+	 * 返回上一页。
+	 */
 	const goBack = () => {
 		uni.navigateBack();
 	};
 
+	/**
+	 * 跳转到积分明细页。
+	 */
 	const goPointDetail = () => {
 		uni.navigateTo({
 			url: '/pages/pointDetail/pointDetail'
@@ -429,4 +585,4 @@
 			margin-right: 8rpx;
 		}
 	}
-</style>
\ No newline at end of file
+</style>
diff --git a/traditionalApp/pages/point/service.js b/traditionalApp/pages/point/service.js
new file mode 100644
index 0000000..d2ce573
--- /dev/null
+++ b/traditionalApp/pages/point/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取 APP 我的积分页面聚合数据。
+ */
+export const getPointsOverview = () => {
+	return request.get('/api/app/points/overview', {}, false, false)
+}
diff --git a/traditionalApp/pages/pointDetail/pointDetail.vue b/traditionalApp/pages/pointDetail/pointDetail.vue
index fc3e931..41c10da 100644
--- a/traditionalApp/pages/pointDetail/pointDetail.vue
+++ b/traditionalApp/pages/pointDetail/pointDetail.vue
@@ -18,59 +18,241 @@
 			</view>
 
 			<!-- 列表区 -->
-			<scroll-view scroll-y class="list-container">
-				<block v-for="(item, index) in listData" :key="index">
+			<scroll-view scroll-y class="list-container" @scrolltolower="handleScrollToLower">
+				<view v-if="!loading && listData.length === 0" class="empty-box">
+					<text class="empty-text">暂无积分明细</text>
+				</view>
+				<block v-for="item in listData" :key="item.id">
 					<view class="list-item">
 						<view class="item-left">
 							<text class="item-title">{{ item.title }}</text>
 							<text class="item-time">{{ item.time }}</text>
 						</view>
 						<view class="item-right">
-							<text :class="['item-score', item.type === 'income' ? 'income' : 'expense']">
-								{{ item.type === 'income' ? '+' : '' }}{{ item.score }}
+							<text :class="['item-score', resolveScoreClass(item)]">
+								{{ formatPoints(item) }}
 							</text>
 						</view>
 					</view>
 				</block>
+				<view v-if="loading" class="footer-box">
+					<text class="footer-text">加载中...</text>
+				</view>
+				<view v-else-if="loadingMore" class="footer-box">
+					<text class="footer-text">加载更多中...</text>
+				</view>
+				<view v-else-if="finished && listData.length > 0" class="footer-box">
+					<text class="footer-text">没有更多了</text>
+				</view>
 			</scroll-view>
 		</view>
 	</view>
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onReachBottom, onShow } from '@dcloudio/uni-app'
+import { getPointsLedgerPage } from './service'
 
-	const statusBarHeight = ref(20);
-	const currentTab = ref(0);
+interface PointsLedgerItem {
+	id: number | null
+	title: string
+	time: string
+	incomeExpenseType: string
+	pointsDelta: number
+}
 
-	const allData = [
-		{ title: '观看课程【课程名称课程名称】', time: '2025.12.20 00:20', score: 50, type: 'income' },
-		{ title: '兑换礼品【礼品名称礼品名称礼品名称】', time: '2025.12.20 00:20', score: -50, type: 'expense' },
-		{ title: '观看课程【课程名称课程名称】', time: '2025.12.20 00:20', score: 50, type: 'income' },
-		{ title: '兑换礼品【礼品名称礼品名称礼品名称】', time: '2025.12.20 00:20', score: -50, type: 'expense' }
-	];
+const statusBarHeight = ref(20)
+const currentTab = ref(0)
+const listData = ref<PointsLedgerItem[]>([])
+const loading = ref(false)
+const loadingMore = ref(false)
+const finished = ref(false)
+const page = ref(1)
+const pageSize =  ref(20)
 
-	const listData = ref(allData);
+/**
+ * 页面加载时初始化状态栏高度。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+})
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+/**
+ * 页面显示时重置分页并刷新积分明细。
+ */
+onShow(() => {
+	loadLedgerPage(true)
+})
 
-	const goBack = () => {
-		uni.navigateBack();
-	};
+/**
+ * 页面触底时继续请求下一页。
+ */
+onReachBottom(() => {
+	loadLedgerPage(false)
+})
 
-	const switchTab = (index: number) => {
-		currentTab.value = index;
-		if (index === 0) {
-			listData.value = allData;
-		} else if (index === 1) {
-			listData.value = allData.filter(item => item.type === 'income');
-		} else {
-			listData.value = allData.filter(item => item.type === 'expense');
+/**
+ * 拉取积分明细分页数据,并在切换筛选时重置列表。
+ */
+async function loadLedgerPage(reset = false) {
+	if (loading.value || loadingMore.value) {
+		return
+	}
+	if (!reset && finished.value) {
+		return
+	}
+	if (reset) {
+		page.value = 1
+		finished.value = false
+		loading.value = true
+	} else {
+		loadingMore.value = true
+	}
+	try {
+		const response = await getPointsLedgerPage({
+			page: page.value,
+			pageSize: pageSize.value,
+			incomeExpenseType: resolveIncomeExpenseType(currentTab.value),
+		})
+		if (response?.code !== 0) {
+			return
 		}
-	};
+		const records = normalizeList(response?.data?.list)
+		const total = Number(response?.data?.total || 0)
+		// 切换 Tab 后需要覆盖旧数据;滚动分页时则拼接新结果。
+		listData.value = reset ? records : listData.value.concat(records)
+		const currentSize = listData.value.length
+		finished.value = records.length < pageSize.value || (total > 0 && currentSize >= total)
+		if (!finished.value) {
+			page.value += 1
+		}
+	} catch {
+		// 请求层会统一处理登录失效与异常提示,这里不重复弹窗。
+	} finally {
+		loading.value = false
+		loadingMore.value = false
+	}
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 切换积分明细类型筛选,并从第一页重新查询。
+ */
+function switchTab(index: number) {
+	if (currentTab.value === index) {
+		return
+	}
+	currentTab.value = index
+	loadLedgerPage(true)
+}
+
+/**
+ * 列表滚动到底部时复用分页加载逻辑。
+ */
+function handleScrollToLower() {
+	loadLedgerPage(false)
+}
+
+/**
+ * 根据当前 Tab 解析接口筛选字段。
+ */
+function resolveIncomeExpenseType(index: number) {
+	if (index === 1) {
+		return 'INCOME'
+	}
+	if (index === 2) {
+		return 'EXPENSE'
+	}
+	return ''
+}
+
+/**
+ * 将接口返回的明细列表归一化为页面展示结构。
+ */
+function normalizeList(value: unknown): PointsLedgerItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const record = (item && typeof item === 'object' ? item : {}) as Record<string, unknown>
+		return {
+			id: normalizeOptionalNumber(record.id),
+			title: normalizeOptionalText(record.title) || '积分变动【-】',
+			time: formatDateTime(record.operatedAt),
+			incomeExpenseType: normalizeOptionalText(record.incomeExpenseType),
+			pointsDelta: normalizeNumber(record.pointsDelta),
+		}
+	})
+}
+
+/**
+ * 将接口时间格式化为页面所需的展示文案。
+ */
+function formatDateTime(value: unknown) {
+	const normalized = normalizeOptionalText(value)
+	if (!normalized) {
+		return '-'
+	}
+	return normalized.slice(0, 16).replace(/-/g, '.').replace('T', ' ')
+}
+
+/**
+ * 将积分数值格式化为带符号的展示文本。
+ */
+function formatPoints(item: PointsLedgerItem) {
+	const points = Math.abs(normalizeNumber(item.pointsDelta))
+	if (resolveScoreClass(item) === 'expense') {
+		return `-${points}`
+	}
+	return `+${points}`
+}
+
+/**
+ * 解析积分变动的颜色类型,收入显示绿色,支出显示红色。
+ */
+function resolveScoreClass(item: PointsLedgerItem) {
+	return item.incomeExpenseType === 'EXPENSE' ? 'expense' : 'income'
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
+
+/**
+ * 将可选数字归一化为数字或空值。
+ */
+function normalizeOptionalNumber(value: unknown) {
+	const parsedValue = Number(value)
+	if (!Number.isFinite(parsedValue)) {
+		return null
+	}
+	return parsedValue
+}
+
+/**
+ * 将任意积分值归一化为数值,避免展示 NaN。
+ */
+function normalizeNumber(value: unknown) {
+	const parsedValue = Number(value)
+	if (!Number.isFinite(parsedValue)) {
+		return 0
+	}
+	return parsedValue
+}
 </script>
 
 <style lang="scss" scoped>
@@ -152,6 +334,18 @@
 		height: 0;
 	}
 
+	.empty-box {
+		padding: 120rpx 0;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.empty-text {
+		font-size: 28rpx;
+		color: #999999;
+	}
+
 	.list-item {
 		width: 100%;
 		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
@@ -201,4 +395,16 @@
 			color: #D32E29;
 		}
 	}
-</style>
\ No newline at end of file
+
+	.footer-box {
+		padding: 16rpx 0 32rpx;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.footer-text {
+		font-size: 24rpx;
+		color: #999999;
+	}
+</style>
diff --git a/traditionalApp/pages/pointDetail/service.js b/traditionalApp/pages/pointDetail/service.js
new file mode 100644
index 0000000..7635023
--- /dev/null
+++ b/traditionalApp/pages/pointDetail/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取 APP 当前登录用户的积分明细分页列表。
+ */
+export const getPointsLedgerPage = (data) => {
+	return request.get('/api/app/points/ledger', data, false, false)
+}
diff --git a/traditionalApp/pages/vipRecord/service.js b/traditionalApp/pages/vipRecord/service.js
new file mode 100644
index 0000000..36facce
--- /dev/null
+++ b/traditionalApp/pages/vipRecord/service.js
@@ -0,0 +1,8 @@
+import request from '@/utils/request.js'
+
+/**
+ * 获取当前登录用户的会员购买记录列表。
+ */
+export const getVipPurchaseOrders = (data) => {
+	return request.get('/api/app/member/purchase-orders', data, false, false)
+}
diff --git a/traditionalApp/pages/vipRecord/vipRecord.vue b/traditionalApp/pages/vipRecord/vipRecord.vue
index afb2469..c1bd1be 100644
--- a/traditionalApp/pages/vipRecord/vipRecord.vue
+++ b/traditionalApp/pages/vipRecord/vipRecord.vue
@@ -1,6 +1,5 @@
 <template>
 	<view class="container">
-		<!-- 自定义导航栏 -->
 		<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
 			<view class="nav-content">
 				<image class="back-icon" src="/static/image/home/left.png" mode="aspectFit" @tap="goBack"></image>
@@ -9,75 +8,24 @@
 			</view>
 		</view>
 
-		<!-- 记录列表 -->
 		<scroll-view scroll-y class="record-list">
-			<!-- 记录项 1 -->
-			<view class="record-item">
-				<view class="item-left">
-					<view class="title-box">
-						<text class="item-title">月卡</text>
-					</view>
-					<text class="item-time">2025.12.20 00:20</text>
-				</view>
-				<view class="item-right">
-					<view class="price-box">
-						<text class="price-symbol">¥</text>
-						<text class="price-num">8888.25</text>
-					</view>
-					<text class="pay-method">微信支付</text>
-				</view>
+			<view v-if="records.length === 0" class="empty-box">
+				<text class="empty-text">暂无购买记录</text>
 			</view>
-
-			<!-- 记录项 2 (带退款标签) -->
-			<view class="record-item">
+			<view v-for="item in records" :key="item.id || item.orderNo" class="record-item">
 				<view class="item-left">
 					<view class="title-box">
-						<text class="item-title">月卡</text>
-						<view class="refund-tag">退款 ¥888.89</view>
+						<text class="item-title">{{ item.memberTypeText }}</text>
+						<view v-if="item.showRefundTag" class="refund-tag">退款 ¥{{ formatPrice(item.refundAmount) }}</view>
 					</view>
-					<text class="item-time">2025.12.20 00:20</text>
+					<text class="item-time">{{ formatDateTime(item.paidAt) }}</text>
 				</view>
 				<view class="item-right">
 					<view class="price-box">
 						<text class="price-symbol">¥</text>
-						<text class="price-num">8888.25</text>
+						<text class="price-num">{{ formatPrice(item.orderAmount) }}</text>
 					</view>
-					<text class="pay-method">微信支付</text>
-				</view>
-			</view>
-
-			<!-- 记录项 3 -->
-			<view class="record-item">
-				<view class="item-left">
-					<view class="title-box">
-						<text class="item-title">月卡</text>
-					</view>
-					<text class="item-time">2025.12.20 00:20</text>
-				</view>
-				<view class="item-right">
-					<view class="price-box">
-						<text class="price-symbol">¥</text>
-						<text class="price-num">8888.25</text>
-					</view>
-					<text class="pay-method">微信支付</text>
-				</view>
-			</view>
-
-			<!-- 记录项 4 (带退款标签) -->
-			<view class="record-item">
-				<view class="item-left">
-					<view class="title-box">
-						<text class="item-title">月卡</text>
-						<view class="refund-tag">退款 ¥888.89</view>
-					</view>
-					<text class="item-time">2025.12.20 00:20</text>
-				</view>
-				<view class="item-right">
-					<view class="price-box">
-						<text class="price-symbol">¥</text>
-						<text class="price-num">8888.25</text>
-					</view>
-					<text class="pay-method">微信支付</text>
+					<text class="pay-method">{{ item.paymentMethodText }}</text>
 				</view>
 			</view>
 		</scroll-view>
@@ -85,144 +33,265 @@
 </template>
 
 <script setup lang="ts">
-	import { ref, onMounted } from 'vue';
+import { ref } from 'vue'
+import { onLoad, onShow } from '@dcloudio/uni-app'
+import { getVipPurchaseOrders } from './service'
 
-	const statusBarHeight = ref(20);
+interface VipPurchaseOrderItem {
+	id?: number | null
+	orderNo?: string
+	memberType?: string
+	memberTypeText?: string
+	orderAmount?: number | string | null
+	paymentMethod?: string
+	paymentMethodText?: string
+	status?: string
+	refundAmount?: number | string | null
+	showRefundTag?: boolean
+	paidAt?: string
+}
 
-	onMounted(() => {
-		const systemInfo = uni.getSystemInfoSync();
-		statusBarHeight.value = systemInfo.statusBarHeight || 20;
-	});
+const statusBarHeight = ref(20)
+const records = ref<VipPurchaseOrderItem[]>([])
 
-	const goBack = () => {
-		uni.navigateBack();
-	};
+/**
+ * 页面加载时初始化状态栏高度。
+ */
+onLoad(() => {
+	const systemInfo = uni.getSystemInfoSync()
+	statusBarHeight.value = systemInfo.statusBarHeight || 20
+})
+
+/**
+ * 页面回显时刷新会员购买记录列表。
+ */
+onShow(() => {
+	loadVipPurchaseOrders()
+})
+
+/**
+ * 拉取当前登录用户的会员购买记录第一页数据。
+ */
+async function loadVipPurchaseOrders() {
+	try {
+		const response = await getVipPurchaseOrders({
+			page: 1,
+			pageSize: 20,
+		})
+		if (response?.code !== 0) {
+			return
+		}
+		records.value = normalizeRecords(response?.data?.list)
+	} catch {
+		// 请求层会统一处理未登录和异常提示,这里不重复 toast。
+	}
+}
+
+/**
+ * 将接口返回的记录列表归一化为页面渲染结构。
+ */
+function normalizeRecords(value: unknown): VipPurchaseOrderItem[] {
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value.map((item) => {
+		const record = (item && typeof item === 'object' ? item : {}) as Partial<VipPurchaseOrderItem>
+		return {
+			id: Number.isFinite(Number(record.id)) ? Number(record.id) : null,
+			orderNo: normalizeOptionalText(record.orderNo),
+			memberType: normalizeOptionalText(record.memberType),
+			memberTypeText: normalizeOptionalText(record.memberTypeText) || '会员',
+			orderAmount: record.orderAmount ?? null,
+			paymentMethod: normalizeOptionalText(record.paymentMethod),
+			paymentMethodText: normalizeOptionalText(record.paymentMethodText) || '-',
+			status: normalizeOptionalText(record.status),
+			refundAmount: record.refundAmount ?? null,
+			showRefundTag: !!record.showRefundTag,
+			paidAt: normalizeOptionalText(record.paidAt),
+		}
+	})
+}
+
+/**
+ * 返回上一页。
+ */
+function goBack() {
+	uni.navigateBack()
+}
+
+/**
+ * 将金额统一格式化为两位小数。
+ */
+function formatPrice(value: number | string | null | undefined) {
+	const amount = Number(value || 0)
+	if (!Number.isFinite(amount)) {
+		return '0.00'
+	}
+	return amount.toFixed(2)
+}
+
+/**
+ * 将接口时间格式化为页面所需的展示文案。
+ */
+function formatDateTime(value?: string) {
+	const normalized = normalizeOptionalText(value)
+	if (!normalized) {
+		return '-'
+	}
+	return normalized.slice(0, 16).replace(/-/g, '.').replace('T', ' ')
+}
+
+/**
+ * 将可选文本归一化为空字符串。
+ */
+function normalizeOptionalText(value: unknown) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
 </script>
 
 <style lang="scss" scoped>
-	page {
-		background-color: #FFFFFF;
-	}
+page {
+	background-color: #FFFFFF;
+}
 
-	.container {
-		width: 750rpx;
-		min-height: 100vh;
-		background-color: #FFFFFF;
-		display: flex;
-		flex-direction: column;
-	}
+.container {
+	width: 750rpx;
+	min-height: 100vh;
+	background-color: #FFFFFF;
+	display: flex;
+	flex-direction: column;
+}
 
-	/* 自定义导航栏 */
-	.custom-navbar {
-		width: 100%;
-		background-color: #FFFFFF;
-		position: relative;
-		z-index: 100;
-	}
+.custom-navbar {
+	width: 100%;
+	background-color: #FFFFFF;
+	position: relative;
+	z-index: 100;
+}
 
-	.nav-content {
-		height: 88rpx;
-		display: flex;
-		align-items: center;
-		justify-content: space-between;
-		padding: 0 32rpx;
-	}
+.nav-content {
+	height: 88rpx;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	padding: 0 32rpx;
+}
 
-	.back-icon {
-		width: 40rpx;
-		height: 40rpx;
-	}
+.back-icon {
+	width: 40rpx;
+	height: 40rpx;
+}
 
-	.nav-title {
-		font-size: 36rpx;
-		color: #333333;
-		font-weight: bold;
-	}
+.nav-title {
+	font-size: 36rpx;
+	color: #333333;
+	font-weight: bold;
+}
 
-	.nav-right-placeholder {
-		width: 40rpx; /* 与 back-icon 宽度一致,保持标题居中 */
-	}
+.nav-right-placeholder {
+	width: 40rpx;
+}
 
-	/* 记录列表 */
-	.record-list {
-		flex: 1;
-		padding: 24rpx 32rpx;
-		box-sizing: border-box;
-	}
+.record-list {
+	flex: 1;
+	padding: 24rpx 32rpx;
+	box-sizing: border-box;
+}
 
-	.record-item {
-		width: 686rpx;
-		height: 140rpx;
-		background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
-		border-radius: 16rpx;
-		margin-bottom: 20rpx;
-		padding: 0 32rpx;
-		box-sizing: border-box;
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
-		/* Add subtle border/shadow if needed to match design perfectly */
-		border: 1rpx solid rgba(249, 246, 239, 0.5);
-	}
+.record-item {
+	width: 686rpx;
+	height: 140rpx;
+	background: linear-gradient(270deg, #F9F6EF 0%, #FFFFFF 100%);
+	border-radius: 16rpx;
+	margin-bottom: 20rpx;
+	padding: 0 32rpx;
+	box-sizing: border-box;
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	border: 1rpx solid rgba(249, 246, 239, 0.5);
+}
 
-	.item-left {
-		display: flex;
-		flex-direction: column;
-		justify-content: center;
-	}
+.item-left {
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	flex: 1;
+	min-width: 0;
+}
 
-	.title-box {
-		display: flex;
-		align-items: center;
-		margin-bottom: 12rpx;
-	}
+.title-box {
+	display: flex;
+	align-items: center;
+	margin-bottom: 12rpx;
+}
 
-	.item-title {
-		font-size: 32rpx;
-		color: #333333;
-		font-weight: bold;
-	}
+.item-title {
+	font-size: 32rpx;
+	color: #333333;
+	font-weight: bold;
+	flex-shrink: 0;
+}
 
-	.refund-tag {
-		margin-left: 16rpx;
-		padding: 2rpx 12rpx;
-		background-color: rgba(211, 46, 41, 0.1);
-		color: #D32E29;
-		font-size: 20rpx;
-		border-radius: 4rpx;
-	}
+.refund-tag {
+	margin-left: 16rpx;
+	padding: 2rpx 12rpx;
+	background-color: rgba(211, 46, 41, 0.1);
+	color: #D32E29;
+	font-size: 20rpx;
+	border-radius: 4rpx;
+	max-width: 280rpx;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	.item-time {
-		font-size: 26rpx;
-		color: #86909C;
-	}
+.item-time {
+	font-size: 26rpx;
+	color: #86909C;
+}
 
-	.item-right {
-		display: flex;
-		flex-direction: column;
-		align-items: flex-end;
-		justify-content: center;
-	}
+.item-right {
+	display: flex;
+	flex-direction: column;
+	align-items: flex-end;
+	justify-content: center;
+	margin-left: 24rpx;
+}
 
-	.price-box {
-		display: flex;
-		align-items: baseline;
-		color: #D32E29;
-		margin-bottom: 12rpx;
-	}
+.price-box {
+	display: flex;
+	align-items: baseline;
+	color: #D32E29;
+	margin-bottom: 12rpx;
+}
 
-	.price-symbol {
-		font-size: 28rpx;
-		font-weight: bold;
-	}
+.price-symbol {
+	font-size: 28rpx;
+	font-weight: bold;
+}
 
-	.price-num {
-		font-size: 36rpx;
-		font-weight: bold;
-	}
+.price-num {
+	font-size: 36rpx;
+	font-weight: bold;
+}
 
-	.pay-method {
-		font-size: 26rpx;
-		color: #86909C;
-	}
+.pay-method {
+	font-size: 26rpx;
+	color: #86909C;
+}
+
+.empty-box {
+	padding-top: 160rpx;
+	display: flex;
+	justify-content: center;
+}
+
+.empty-text {
+	font-size: 28rpx;
+	color: #86909C;
+}
 </style>
diff --git a/traditionalApp/utils/download-task-storage.js b/traditionalApp/utils/download-task-storage.js
new file mode 100644
index 0000000..4a13c98
--- /dev/null
+++ b/traditionalApp/utils/download-task-storage.js
@@ -0,0 +1,142 @@
+const STORAGE_PREFIX = 'appHealthCourseDownloadTasks_'
+const DOWNLOAD_TASK_CHANGE_EVENT = 'app-download-task-change'
+
+/**
+ * 构建当前登录用户的下载任务缓存键。
+ */
+export function getDownloadTaskStorageKey(userId) {
+	const normalizedUserId = Number(userId || 0)
+	return `${STORAGE_PREFIX}${Number.isFinite(normalizedUserId) ? normalizedUserId : 0}`
+}
+
+/**
+ * 读取当前登录用户的下载中任务列表。
+ */
+export function listDownloadTasks(userId) {
+	const storageKey = getDownloadTaskStorageKey(userId)
+	const value = uni.getStorageSync(storageKey)
+	if (!Array.isArray(value)) {
+		return []
+	}
+	return value
+		.map((item) => normalizeDownloadTask(item))
+		.filter((item) => !!item)
+}
+
+/**
+ * 写入或刷新一条下载任务。
+ */
+export function upsertDownloadTask(userId, task) {
+	const nextTask = normalizeDownloadTask(task)
+	if (!nextTask) {
+		return
+	}
+	const taskList = listDownloadTasks(userId)
+	const targetIndex = taskList.findIndex((item) => item.clientTaskId === nextTask.clientTaskId)
+	if (targetIndex >= 0) {
+		taskList.splice(targetIndex, 1, nextTask)
+	} else {
+		taskList.unshift(nextTask)
+	}
+	saveDownloadTasks(userId, taskList)
+}
+
+/**
+ * 批量移除指定下载任务。
+ */
+export function removeDownloadTasks(userId, clientTaskIds) {
+	if (!Array.isArray(clientTaskIds) || clientTaskIds.length === 0) {
+		return
+	}
+	const taskIdSet = new Set(clientTaskIds.map((item) => String(item)))
+	const taskList = listDownloadTasks(userId).filter((item) => !taskIdSet.has(String(item.clientTaskId)))
+	saveDownloadTasks(userId, taskList)
+}
+
+/**
+ * 移除单条下载任务。
+ */
+export function clearDownloadTask(userId, clientTaskId) {
+	removeDownloadTasks(userId, [clientTaskId])
+}
+
+/**
+ * 获取下载任务变更事件名。
+ */
+export function getDownloadTaskChangeEventName() {
+	return DOWNLOAD_TASK_CHANGE_EVENT
+}
+
+/**
+ * 统一落盘下载任务并通知页面刷新。
+ */
+function saveDownloadTasks(userId, taskList) {
+	const storageKey = getDownloadTaskStorageKey(userId)
+	uni.setStorageSync(storageKey, Array.isArray(taskList) ? taskList : [])
+	uni.$emit(DOWNLOAD_TASK_CHANGE_EVENT)
+}
+
+/**
+ * 归一化下载任务结构,避免页面渲染出现脏数据。
+ */
+function normalizeDownloadTask(value) {
+	if (!value || typeof value !== 'object') {
+		return null
+	}
+	const clientTaskId = normalizeText(value.clientTaskId)
+	if (!clientTaskId) {
+		return null
+	}
+	return {
+		clientTaskId,
+		courseId: normalizeNumber(value.courseId),
+		courseVideoId: normalizeNumber(value.courseVideoId),
+		title: normalizeText(value.title),
+		subtitle: normalizeText(value.subtitle),
+		coverImage: normalizeText(value.coverImage),
+		durationSeconds: normalizeNumber(value.durationSeconds),
+		durationText: normalizeText(value.durationText),
+		videoUrl: normalizeText(value.videoUrl),
+		progress: normalizeProgress(value.progress),
+		status: 'downloading',
+		updatedAt: normalizeText(value.updatedAt) || new Date().toISOString(),
+	}
+}
+
+/**
+ * 归一化可选文本。
+ */
+function normalizeText(value) {
+	if (typeof value !== 'string') {
+		return ''
+	}
+	return value.trim()
+}
+
+/**
+ * 归一化数字字段。
+ */
+function normalizeNumber(value) {
+	const numberValue = Number(value)
+	if (!Number.isFinite(numberValue)) {
+		return 0
+	}
+	return numberValue
+}
+
+/**
+ * 归一化下载进度,保证范围在 0 到 100 之间。
+ */
+function normalizeProgress(value) {
+	const progressValue = Math.round(Number(value || 0))
+	if (!Number.isFinite(progressValue)) {
+		return 0
+	}
+	if (progressValue < 0) {
+		return 0
+	}
+	if (progressValue > 100) {
+		return 100
+	}
+	return progressValue
+}

--
Gitblit v1.7.1