“mitao”
2026-04-27 69c2c400e6e3393424d704742cd9de0e6da07e2f
fix:ios授权登录
5个文件已添加
16个文件已修改
891 ■■■■■ 已修改文件
tcm-training-admin-backend/pom.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/20260427_app_apple_login.sql 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/init.sql 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/mock_points_exchange_data.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/mock_user_statistics_recent_week.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/sql/tcm_training_admin.sql 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAppleAuthProperties.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppAppleLoginRequest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAppleAuthClient.java 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAppleUserProfile.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/entity/AppUser.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/row/UserDetailRow.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/application.yml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/resources/mapper/UserMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/manifest.json 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/login.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/login/service.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/pom.xml
@@ -117,6 +117,11 @@
            <artifactId>jsoup</artifactId>
            <version>1.17.2</version>
        </dependency>
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.37.3</version>
        </dependency>
        <!-- Source: https://mvnrepository.com/artifact/com.huaweicloud/esdk-obs-java-bundle -->
        <dependency>
tcm-training-admin-backend/sql/20260427_app_apple_login.sql
New file
@@ -0,0 +1,53 @@
SET @apple_user_identifier_column_exists = (
  SELECT COUNT(*)
  FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA = DATABASE()
    AND TABLE_NAME = 'tcm_app_user'
    AND COLUMN_NAME = 'apple_user_identifier'
);
SET @apple_user_identifier_alter_sql = IF(
  @apple_user_identifier_column_exists = 0,
  'ALTER TABLE `tcm_app_user` ADD COLUMN `apple_user_identifier` VARCHAR(128) DEFAULT NULL COMMENT ''苹果用户标识'' AFTER `wechat_bound_at`',
  'SELECT 1'
);
PREPARE apple_user_identifier_stmt FROM @apple_user_identifier_alter_sql;
EXECUTE apple_user_identifier_stmt;
DEALLOCATE PREPARE apple_user_identifier_stmt;
SET @apple_identity_subject_column_exists = (
  SELECT COUNT(*)
  FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA = DATABASE()
    AND TABLE_NAME = 'tcm_app_user'
    AND COLUMN_NAME = 'apple_identity_subject'
);
SET @apple_identity_subject_alter_sql = IF(
  @apple_identity_subject_column_exists = 0,
  'ALTER TABLE `tcm_app_user` ADD COLUMN `apple_identity_subject` VARCHAR(128) DEFAULT NULL COMMENT ''苹果身份主题标识'' AFTER `apple_user_identifier`',
  'SELECT 1'
);
PREPARE apple_identity_subject_stmt FROM @apple_identity_subject_alter_sql;
EXECUTE apple_identity_subject_stmt;
DEALLOCATE PREPARE apple_identity_subject_stmt;
SET @apple_bound_at_column_exists = (
  SELECT COUNT(*)
  FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA = DATABASE()
    AND TABLE_NAME = 'tcm_app_user'
    AND COLUMN_NAME = 'apple_bound_at'
);
SET @apple_bound_at_alter_sql = IF(
  @apple_bound_at_column_exists = 0,
  'ALTER TABLE `tcm_app_user` ADD COLUMN `apple_bound_at` DATETIME DEFAULT NULL COMMENT ''苹果绑定时间'' AFTER `apple_identity_subject`',
  'SELECT 1'
);
PREPARE apple_bound_at_stmt FROM @apple_bound_at_alter_sql;
EXECUTE apple_bound_at_stmt;
DEALLOCATE PREPARE apple_bound_at_stmt;
tcm-training-admin-backend/sql/init.sql
@@ -300,6 +300,9 @@
  `wechat_nickname` VARCHAR(64) DEFAULT NULL COMMENT '微信昵称',
  `wechat_avatar` VARCHAR(255) DEFAULT NULL COMMENT '微信头像URL',
  `wechat_bound_at` DATETIME DEFAULT NULL COMMENT '微信绑定时间',
  `apple_user_identifier` VARCHAR(128) DEFAULT NULL COMMENT '苹果用户标识',
  `apple_identity_subject` VARCHAR(128) DEFAULT NULL COMMENT '苹果身份主题标识',
  `apple_bound_at` DATETIME DEFAULT NULL COMMENT '苹果绑定时间',
  `type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '用户类型:0个人 1诊所',
  `gender` TINYINT(1) DEFAULT NULL COMMENT '性别:0女 1男',
  `age` TINYINT UNSIGNED DEFAULT NULL COMMENT '年龄',
@@ -319,8 +322,8 @@
  KEY `idx_app_user_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='APP用户表(C端用户)';
INSERT IGNORE INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`)
VALUES (200, '张三', '13800008888', NULL, 'https://example.com/avatar/app-user-200.png', 'wx_openid_200', '小张', 'https://example.com/avatar/wechat-200.png', CURRENT_TIMESTAMP, 1, 1, 19, 'If you find an element of your interface requires instructions, then you need to redesign it.', 1, 1, NULL, NULL, 0);
INSERT IGNORE INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`)
VALUES (200, '张三', '13800008888', NULL, 'https://example.com/avatar/app-user-200.png', 'wx_openid_200', '小张', 'https://example.com/avatar/wechat-200.png', CURRENT_TIMESTAMP, NULL, NULL, NULL, 1, 1, 19, 'If you find an element of your interface requires instructions, then you need to redesign it.', 1, 1, NULL, NULL, 0);
-- ----------------------------
-- 会员价格配置
tcm-training-admin-backend/sql/mock_points_exchange_data.sql
@@ -36,15 +36,15 @@
-- 用户
INSERT INTO `tcm_app_user`
(`id`, `nickname`, `phone`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`)
(`id`, `nickname`, `phone`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`)
VALUES
(9001, '岐黄学员A', '13900000001', 'https://example.com/mock/avatar-9001.png', 'mock_openid_9001', '岐黄学员A', 'https://example.com/mock/wx-9001.png', '2026-03-01 09:00:00', 1, 1, 32, '积分兑换 mock 用户 A', 1, 8001, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9002, '岐黄学员B', '13900000002', 'https://example.com/mock/avatar-9002.png', 'mock_openid_9002', '岐黄学员B', 'https://example.com/mock/wx-9002.png', '2026-03-02 09:00:00', 0, 0, 29, '积分兑换 mock 用户 B', 1, 8001, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9003, '岐黄学员C', '13900000003', 'https://example.com/mock/avatar-9003.png', 'mock_openid_9003', '岐黄学员C', 'https://example.com/mock/wx-9003.png', '2026-03-03 09:00:00', 1, 1, 35, '积分兑换 mock 用户 C', 1, 8002, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9004, '岐黄学员D', '13900000004', 'https://example.com/mock/avatar-9004.png', 'mock_openid_9004', '岐黄学员D', 'https://example.com/mock/wx-9004.png', '2026-03-04 09:00:00', 0, 0, 27, '积分兑换 mock 用户 D', 1, 8002, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9005, '岐黄学员E', '13900000005', 'https://example.com/mock/avatar-9005.png', 'mock_openid_9005', '岐黄学员E', 'https://example.com/mock/wx-9005.png', '2026-03-05 09:00:00', 1, 1, 41, '积分兑换 mock 用户 E', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9006, '岐黄学员F', '13900000006', 'https://example.com/mock/avatar-9006.png', 'mock_openid_9006', '岐黄学员F', 'https://example.com/mock/wx-9006.png', '2026-03-06 09:00:00', 0, 0, 38, '积分兑换 mock 用户 F', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9007, '岐黄学员G', '13900000007', 'https://example.com/mock/avatar-9007.png', 'mock_openid_9007', '岐黄学员G', 'https://example.com/mock/wx-9007.png', '2026-03-07 09:00:00', 0, 1, 30, '积分兑换 mock 用户 G', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock');
(9001, '岐黄学员A', '13900000001', 'https://example.com/mock/avatar-9001.png', 'mock_openid_9001', '岐黄学员A', 'https://example.com/mock/wx-9001.png', '2026-03-01 09:00:00', NULL, NULL, NULL, 1, 1, 32, '积分兑换 mock 用户 A', 1, 8001, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9002, '岐黄学员B', '13900000002', 'https://example.com/mock/avatar-9002.png', 'mock_openid_9002', '岐黄学员B', 'https://example.com/mock/wx-9002.png', '2026-03-02 09:00:00', NULL, NULL, NULL, 0, 0, 29, '积分兑换 mock 用户 B', 1, 8001, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9003, '岐黄学员C', '13900000003', 'https://example.com/mock/avatar-9003.png', 'mock_openid_9003', '岐黄学员C', 'https://example.com/mock/wx-9003.png', '2026-03-03 09:00:00', NULL, NULL, NULL, 1, 1, 35, '积分兑换 mock 用户 C', 1, 8002, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9004, '岐黄学员D', '13900000004', 'https://example.com/mock/avatar-9004.png', 'mock_openid_9004', '岐黄学员D', 'https://example.com/mock/wx-9004.png', '2026-03-04 09:00:00', NULL, NULL, NULL, 0, 0, 27, '积分兑换 mock 用户 D', 1, 8002, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9005, '岐黄学员E', '13900000005', 'https://example.com/mock/avatar-9005.png', 'mock_openid_9005', '岐黄学员E', 'https://example.com/mock/wx-9005.png', '2026-03-05 09:00:00', NULL, NULL, NULL, 1, 1, 41, '积分兑换 mock 用户 E', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9006, '岐黄学员F', '13900000006', 'https://example.com/mock/avatar-9006.png', 'mock_openid_9006', '岐黄学员F', 'https://example.com/mock/wx-9006.png', '2026-03-06 09:00:00', NULL, NULL, NULL, 0, 0, 38, '积分兑换 mock 用户 F', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock'),
(9007, '岐黄学员G', '13900000007', 'https://example.com/mock/avatar-9007.png', 'mock_openid_9007', '岐黄学员G', 'https://example.com/mock/wx-9007.png', '2026-03-07 09:00:00', NULL, NULL, NULL, 0, 1, 30, '积分兑换 mock 用户 G', 1, 8003, '2027-03-01 00:00:00', 0, 'mock', 'mock');
-- 积分设置
INSERT INTO `tcm_points_config`
tcm-training-admin-backend/sql/mock_user_statistics_recent_week.sql
@@ -4,22 +4,22 @@
INSERT IGNORE INTO `tcm_app_user`
(`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`,
 `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `member_expire_at`, `deleted`, `created_at`, `updated_at`)
 `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `member_expire_at`, `deleted`, `created_at`, `updated_at`)
VALUES
(301, '李安然', '13800009001', NULL, 'https://example.com/avatar/app-user-301.png', 'wx_openid_301', '安然', 'https://example.com/avatar/wechat-301.png', '2026-03-20 09:10:00',
 0, 0, 28, '最近开始关注养生课程。', 1, NULL, NULL, 0, '2026-03-20 09:00:00', '2026-03-20 09:00:00'),
 NULL, NULL, NULL, 0, 0, 28, '最近开始关注养生课程。', 1, NULL, NULL, 0, '2026-03-20 09:00:00', '2026-03-20 09:00:00'),
(302, '陈思远', '13800009002', NULL, 'https://example.com/avatar/app-user-302.png', 'wx_openid_302', '思远', 'https://example.com/avatar/wechat-302.png', '2026-03-21 10:25:00',
 1, 1, 36, '诊所负责人,关注课程内容。', 1, 1, NULL, 0, '2026-03-21 10:00:00', '2026-03-21 10:00:00'),
 NULL, NULL, NULL, 1, 1, 36, '诊所负责人,关注课程内容。', 1, 1, NULL, 0, '2026-03-21 10:00:00', '2026-03-21 10:00:00'),
(303, '周雨桐', '13800009003', NULL, 'https://example.com/avatar/app-user-303.png', 'wx_openid_303', '雨桐', 'https://example.com/avatar/wechat-303.png', '2026-03-22 14:10:00',
 0, 0, 31, '已经开通过月卡会员。', 1, NULL, '2026-05-15 23:59:59', 0, '2026-03-22 13:30:00', '2026-03-22 13:30:00'),
 NULL, NULL, NULL, 0, 0, 31, '已经开通过月卡会员。', 1, NULL, '2026-05-15 23:59:59', 0, '2026-03-22 13:30:00', '2026-03-22 13:30:00'),
(304, '王知夏', '13800009004', NULL, 'https://example.com/avatar/app-user-304.png', 'wx_openid_304', '知夏', 'https://example.com/avatar/wechat-304.png', '2026-03-23 11:25:00',
 0, 1, 27, '偏好购买课程小节。', 1, NULL, '2026-06-30 23:59:59', 0, '2026-03-23 11:00:00', '2026-03-23 11:00:00'),
 NULL, NULL, NULL, 0, 1, 27, '偏好购买课程小节。', 1, NULL, '2026-06-30 23:59:59', 0, '2026-03-23 11:00:00', '2026-03-23 11:00:00'),
(305, '赵景行', '13800009005', NULL, 'https://example.com/avatar/app-user-305.png', 'wx_openid_305', '景行', 'https://example.com/avatar/wechat-305.png', '2026-03-24 16:18:00',
 1, 1, 40, '新注册的诊所用户。', 1, 1, NULL, 0, '2026-03-24 16:00:00', '2026-03-24 16:00:00'),
 NULL, NULL, NULL, 1, 1, 40, '新注册的诊所用户。', 1, 1, NULL, 0, '2026-03-24 16:00:00', '2026-03-24 16:00:00'),
(306, '许清禾', '13800009006', NULL, 'https://example.com/avatar/app-user-306.png', 'wx_openid_306', '清禾', 'https://example.com/avatar/wechat-306.png', '2026-03-25 09:22:00',
 0, 0, 33, '季卡会员用户。', 1, NULL, '2026-07-31 23:59:59', 0, '2026-03-25 09:00:00', '2026-03-25 09:00:00'),
 NULL, NULL, NULL, 0, 0, 33, '季卡会员用户。', 1, NULL, '2026-07-31 23:59:59', 0, '2026-03-25 09:00:00', '2026-03-25 09:00:00'),
(307, '孙书意', '13800009007', NULL, 'https://example.com/avatar/app-user-307.png', 'wx_openid_307', '书意', 'https://example.com/avatar/wechat-307.png', '2026-03-26 20:08:00',
 0, 0, 25, '刚注册,主要浏览直播回放。', 1, NULL, NULL, 0, '2026-03-26 19:30:00', '2026-03-26 19:30:00');
 NULL, NULL, NULL, 0, 0, 25, '刚注册,主要浏览直播回放。', 1, NULL, NULL, 0, '2026-03-26 19:30:00', '2026-03-26 19:30:00');
INSERT IGNORE INTO `tcm_live_session`
(`id`, `live_course_id`, `start_time`, `video_url`, `base_reserve_count`, `base_watch_count`, `real_reserve_count`, `real_watch_count`,
tcm-training-admin-backend/sql/tcm_training_admin.sql
@@ -4089,6 +4089,9 @@
  `wechat_nickname` varchar(64) DEFAULT NULL COMMENT '微信昵称',
  `wechat_avatar` varchar(255) DEFAULT NULL COMMENT '微信头像URL',
  `wechat_bound_at` datetime DEFAULT NULL COMMENT '微信绑定时间',
  `apple_user_identifier` varchar(128) DEFAULT NULL COMMENT '苹果用户标识',
  `apple_identity_subject` varchar(128) DEFAULT NULL COMMENT '苹果身份主题标识',
  `apple_bound_at` datetime DEFAULT NULL COMMENT '苹果绑定时间',
  `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '用户类型:0个人 1诊所',
  `gender` tinyint(1) DEFAULT NULL COMMENT '性别:0女 1男',
  `age` tinyint unsigned DEFAULT NULL COMMENT '年龄',
@@ -4112,23 +4115,23 @@
-- Records of tcm_app_user
-- ----------------------------
BEGIN;
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (200, '张三', '138****8888', NULL, 'http://localhost:48080/uploads/2026/03/17/6b8537ae6cb544b7a5fac0280d493e08.png', NULL, NULL, NULL, NULL, 1, 1, 19, 'If you find an element of your interface requires instructions, then you need to redesign it.', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-17 08:12:59', '2026-03-17 08:12:59');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (301, '李安然', '13800009001', NULL, 'https://example.com/avatar/app-user-301.png', 'wx_openid_301', '安然', 'https://example.com/avatar/wechat-301.png', '2026-03-20 09:10:00', 0, 0, 28, '最近开始关注养生课程。', 1, NULL, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-20 09:00:00', '2026-03-20 09:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (302, '陈思远', '13800009002', NULL, 'https://example.com/avatar/app-user-302.png', 'wx_openid_302', '思远', 'https://example.com/avatar/wechat-302.png', '2026-03-21 10:25:00', 1, 1, 36, '诊所负责人,关注课程内容。', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-21 10:00:00', '2026-03-21 10:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (303, '周雨桐', '13800009003', NULL, 'https://example.com/avatar/app-user-303.png', 'wx_openid_303', '雨桐', 'https://example.com/avatar/wechat-303.png', '2026-03-22 14:10:00', 0, 0, 31, '已经开通过月卡会员。', 1, NULL, NULL, '2026-05-15 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-22 13:30:00', '2026-03-22 13:30:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (304, '王知夏', '13800009004', NULL, 'https://example.com/avatar/app-user-304.png', 'wx_openid_304', '知夏', 'https://example.com/avatar/wechat-304.png', '2026-03-23 11:25:00', 0, 1, 27, '偏好购买课程小节。', 1, NULL, NULL, '2026-06-30 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-23 11:00:00', '2026-03-23 11:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (305, '赵景行', '13800009005', NULL, 'https://example.com/avatar/app-user-305.png', 'wx_openid_305', '景行', 'https://example.com/avatar/wechat-305.png', '2026-03-24 16:18:00', 1, 1, 40, '新注册的诊所用户。', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-24 16:00:00', '2026-03-24 16:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (306, '许清禾', '13800009006', NULL, 'https://example.com/avatar/app-user-306.png', 'wx_openid_306', '清禾', 'https://example.com/avatar/wechat-306.png', '2026-03-25 09:22:00', 0, 0, 33, '季卡会员用户。', 1, NULL, NULL, '2026-07-31 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-25 09:00:00', '2026-03-25 09:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (307, '孙书意', '13800009007', NULL, 'https://example.com/avatar/app-user-307.png', 'wx_openid_307', '书意', 'https://example.com/avatar/wechat-307.png', '2026-03-26 20:08:00', 0, 0, 25, '刚注册,主要浏览直播回放。', 1, NULL, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-26 19:30:00', '2026-03-26 19:30:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9001, '岐黄学员A', '13900000001', NULL, 'https://example.com/mock/avatar-9001.png', 'mock_openid_9001', '岐黄学员A', 'https://example.com/mock/wx-9001.png', '2026-03-01 09:00:00', 1, 1, 32, '积分兑换 mock 用户 A', 1, 8001, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9002, '岐黄学员B', '13900000002', NULL, 'https://example.com/mock/avatar-9002.png', 'mock_openid_9002', '岐黄学员B', 'https://example.com/mock/wx-9002.png', '2026-03-02 09:00:00', 0, 0, 29, '积分兑换 mock 用户 B', 1, 8001, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9003, '岐黄学员C', '13900000003', NULL, 'https://example.com/mock/avatar-9003.png', 'mock_openid_9003', '岐黄学员C', 'https://example.com/mock/wx-9003.png', '2026-03-03 09:00:00', 1, 1, 35, '积分兑换 mock 用户 C', 1, 8002, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9004, '岐黄学员D', '13900000004', NULL, 'https://example.com/mock/avatar-9004.png', 'mock_openid_9004', '岐黄学员D', 'https://example.com/mock/wx-9004.png', '2026-03-04 09:00:00', 0, 0, 27, '积分兑换 mock 用户 D', 1, 8002, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9005, '岐黄学员E', '13900000005', NULL, 'https://example.com/mock/avatar-9005.png', 'mock_openid_9005', '岐黄学员E', 'https://example.com/mock/wx-9005.png', '2026-03-05 09:00:00', 1, 1, 41, '积分兑换 mock 用户 E', 1, 8003, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9006, '岐黄学员F', '13900000006', NULL, 'https://example.com/mock/avatar-9006.png', 'mock_openid_9006', '岐黄学员F', 'https://example.com/mock/wx-9006.png', '2026-03-06 09:00:00', 0, 0, 38, '积分兑换 mock 用户 F', 1, 8003, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9007, '岐黄学员G', '13900000007', NULL, 'https://haizhengkangdao.obs.cn-south-1.myhuaweicloud.com/image/2026/03/27/0aecfb7a93d14646a119387b4830320b.jpg', 'mock_openid_9007', '岐黄学员G', 'https://example.com/mock/wx-9007.png', '2026-03-07 09:00:00', 0, 1, 30, '积分兑换 mock 用户 G', 1, 8003, NULL, '2027-03-01 23:59:59', 0, 'mock', '张三', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9008, '测试云仓', '18224358733', '$2a$10$VOeFYDJHIZO7P6Ne.JUGsO.VJ5Coa/d4Qp8KcIv1GjJT2hnrRp4u.', NULL, NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, 1, 8004, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-04-07 07:49:35', '2026-04-07 07:49:35');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9009, '183****8484', '18398968484', '$2a$10$OKhltkW73TnWIOzVpKvi5ukjyY9p2khtcAUz9Ps3PWWKpzyxOcOu6', 'https://haizhentong.obs.cn-south-1.myhuaweicloud.com/file/773935d627384a0f888d8c292bca493d.jpg', NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 1, 8004, 9008, NULL, 0, '系统初始化', '系统初始化', '2026-04-07 08:37:16', '2026-04-07 08:37:16');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (200, '张三', '138****8888', NULL, 'http://localhost:48080/uploads/2026/03/17/6b8537ae6cb544b7a5fac0280d493e08.png', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, 1, 19, 'If you find an element of your interface requires instructions, then you need to redesign it.', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-17 08:12:59', '2026-03-17 08:12:59');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (301, '李安然', '13800009001', NULL, 'https://example.com/avatar/app-user-301.png', 'wx_openid_301', '安然', 'https://example.com/avatar/wechat-301.png', '2026-03-20 09:10:00', NULL, NULL, NULL, 0, 0, 28, '最近开始关注养生课程。', 1, NULL, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-20 09:00:00', '2026-03-20 09:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (302, '陈思远', '13800009002', NULL, 'https://example.com/avatar/app-user-302.png', 'wx_openid_302', '思远', 'https://example.com/avatar/wechat-302.png', '2026-03-21 10:25:00', NULL, NULL, NULL, 1, 1, 36, '诊所负责人,关注课程内容。', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-21 10:00:00', '2026-03-21 10:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (303, '周雨桐', '13800009003', NULL, 'https://example.com/avatar/app-user-303.png', 'wx_openid_303', '雨桐', 'https://example.com/avatar/wechat-303.png', '2026-03-22 14:10:00', NULL, NULL, NULL, 0, 0, 31, '已经开通过月卡会员。', 1, NULL, NULL, '2026-05-15 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-22 13:30:00', '2026-03-22 13:30:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (304, '王知夏', '13800009004', NULL, 'https://example.com/avatar/app-user-304.png', 'wx_openid_304', '知夏', 'https://example.com/avatar/wechat-304.png', '2026-03-23 11:25:00', NULL, NULL, NULL, 0, 1, 27, '偏好购买课程小节。', 1, NULL, NULL, '2026-06-30 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-23 11:00:00', '2026-03-23 11:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (305, '赵景行', '13800009005', NULL, 'https://example.com/avatar/app-user-305.png', 'wx_openid_305', '景行', 'https://example.com/avatar/wechat-305.png', '2026-03-24 16:18:00', NULL, NULL, NULL, 1, 1, 40, '新注册的诊所用户。', 1, 1, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-24 16:00:00', '2026-03-24 16:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (306, '许清禾', '13800009006', NULL, 'https://example.com/avatar/app-user-306.png', 'wx_openid_306', '清禾', 'https://example.com/avatar/wechat-306.png', '2026-03-25 09:22:00', NULL, NULL, NULL, 0, 0, 33, '季卡会员用户。', 1, NULL, NULL, '2026-07-31 23:59:59', 0, '系统初始化', '系统初始化', '2026-03-25 09:00:00', '2026-03-25 09:00:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (307, '孙书意', '13800009007', NULL, 'https://example.com/avatar/app-user-307.png', 'wx_openid_307', '书意', 'https://example.com/avatar/wechat-307.png', '2026-03-26 20:08:00', NULL, NULL, NULL, 0, 0, 25, '刚注册,主要浏览直播回放。', 1, NULL, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-03-26 19:30:00', '2026-03-26 19:30:00');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9001, '岐黄学员A', '13900000001', NULL, 'https://example.com/mock/avatar-9001.png', 'mock_openid_9001', '岐黄学员A', 'https://example.com/mock/wx-9001.png', '2026-03-01 09:00:00', NULL, NULL, NULL, 1, 1, 32, '积分兑换 mock 用户 A', 1, 8001, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9002, '岐黄学员B', '13900000002', NULL, 'https://example.com/mock/avatar-9002.png', 'mock_openid_9002', '岐黄学员B', 'https://example.com/mock/wx-9002.png', '2026-03-02 09:00:00', NULL, NULL, NULL, 0, 0, 29, '积分兑换 mock 用户 B', 1, 8001, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9003, '岐黄学员C', '13900000003', NULL, 'https://example.com/mock/avatar-9003.png', 'mock_openid_9003', '岐黄学员C', 'https://example.com/mock/wx-9003.png', '2026-03-03 09:00:00', NULL, NULL, NULL, 1, 1, 35, '积分兑换 mock 用户 C', 1, 8002, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9004, '岐黄学员D', '13900000004', NULL, 'https://example.com/mock/avatar-9004.png', 'mock_openid_9004', '岐黄学员D', 'https://example.com/mock/wx-9004.png', '2026-03-04 09:00:00', NULL, NULL, NULL, 0, 0, 27, '积分兑换 mock 用户 D', 1, 8002, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9005, '岐黄学员E', '13900000005', NULL, 'https://example.com/mock/avatar-9005.png', 'mock_openid_9005', '岐黄学员E', 'https://example.com/mock/wx-9005.png', '2026-03-05 09:00:00', NULL, NULL, NULL, 1, 1, 41, '积分兑换 mock 用户 E', 1, 8003, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9006, '岐黄学员F', '13900000006', NULL, 'https://example.com/mock/avatar-9006.png', 'mock_openid_9006', '岐黄学员F', 'https://example.com/mock/wx-9006.png', '2026-03-06 09:00:00', NULL, NULL, NULL, 0, 0, 38, '积分兑换 mock 用户 F', 1, 8003, NULL, '2027-03-01 00:00:00', 0, 'mock', 'mock', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9007, '岐黄学员G', '13900000007', NULL, 'https://haizhengkangdao.obs.cn-south-1.myhuaweicloud.com/image/2026/03/27/0aecfb7a93d14646a119387b4830320b.jpg', 'mock_openid_9007', '岐黄学员G', 'https://example.com/mock/wx-9007.png', '2026-03-07 09:00:00', NULL, NULL, NULL, 0, 1, 30, '积分兑换 mock 用户 G', 1, 8003, NULL, '2027-03-01 23:59:59', 0, 'mock', '张三', '2026-03-23 11:00:20', '2026-03-23 11:00:20');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9008, '测试云仓', '18224358733', '$2a$10$VOeFYDJHIZO7P6Ne.JUGsO.VJ5Coa/d4Qp8KcIv1GjJT2hnrRp4u.', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, 1, 8004, NULL, NULL, 0, '系统初始化', '系统初始化', '2026-04-07 07:49:35', '2026-04-07 07:49:35');
INSERT INTO `tcm_app_user` (`id`, `nickname`, `phone`, `password`, `avatar`, `wechat_openid`, `wechat_nickname`, `wechat_avatar`, `wechat_bound_at`, `apple_user_identifier`, `apple_identity_subject`, `apple_bound_at`, `type`, `gender`, `age`, `intro`, `status`, `clinic_id`, `superior_clinic_user_id`, `member_expire_at`, `deleted`, `created_by`, `updated_by`, `created_at`, `updated_at`) VALUES (9009, '183****8484', '18398968484', '$2a$10$OKhltkW73TnWIOzVpKvi5ukjyY9p2khtcAUz9Ps3PWWKpzyxOcOu6', 'https://haizhentong.obs.cn-south-1.myhuaweicloud.com/file/773935d627384a0f888d8c292bca493d.jpg', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 1, NULL, NULL, 1, 8004, 9008, NULL, 0, '系统初始化', '系统初始化', '2026-04-07 08:37:16', '2026-04-07 08:37:16');
COMMIT;
-- ----------------------------
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/config/AppAppleAuthProperties.java
New file
@@ -0,0 +1,72 @@
package com.tcm.training.admin.app.auth.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * APP 苹果登录配置。
 */
@Component
@ConfigurationProperties(prefix = "app.auth.apple")
public class AppAppleAuthProperties {
    private String clientId = "";
    private String issuer = "https://appleid.apple.com";
    private String jwksUrl = "https://appleid.apple.com/auth/keys";
    /**
     * 获取苹果登录 clientId。
     */
    public String getClientId() {
        return clientId;
    }
    /**
     * 设置苹果登录 clientId。
     */
    public void setClientId(String clientId) {
        this.clientId = clientId;
    }
    /**
     * 获取苹果签发方。
     */
    public String getIssuer() {
        return issuer;
    }
    /**
     * 设置苹果签发方。
     */
    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }
    /**
     * 获取苹果 JWKS 地址。
     */
    public String getJwksUrl() {
        return jwksUrl;
    }
    /**
     * 设置苹果 JWKS 地址。
     */
    public void setJwksUrl(String jwksUrl) {
        this.jwksUrl = jwksUrl;
    }
    /**
     * 判断是否已配置服务端验签所需参数。
     */
    public boolean isConfigured() {
        return hasText(clientId) && hasText(issuer) && hasText(jwksUrl);
    }
    /**
     * 判断字符串是否包含有效文本。
     */
    private boolean hasText(String value) {
        return value != null && !value.trim().isEmpty();
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/controller/AppAuthController.java
@@ -1,6 +1,7 @@
package com.tcm.training.admin.app.auth.controller;
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
import com.tcm.training.admin.app.auth.dto.AppAppleLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest;
@@ -69,6 +70,14 @@
    }
    /**
     * 使用苹果授权信息完成 APP 登录。
     */
    @PostMapping("/apple-login")
    public ApiResponse<AppLoginResponse> appleLogin(@Valid @RequestBody AppAppleLoginRequest request) {
        return ApiResponse.ok(appAuthService.appleLogin(request));
    }
    /**
     * 发送 APP 忘记密码验证码。
     */
    @PostMapping("/password-reset/code")
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/dto/AppAppleLoginRequest.java
New file
@@ -0,0 +1,23 @@
package com.tcm.training.admin.app.auth.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
 * APP 苹果登录请求。
 */
@Getter
@Setter
@NoArgsConstructor
public class AppAppleLoginRequest {
    @NotBlank(message = "苹果授权信息不完整")
    private String identityToken;
    private String authorizationCode;
    private String userIdentifier;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAppleAuthClient.java
New file
@@ -0,0 +1,211 @@
package com.tcm.training.admin.app.auth.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.tcm.training.admin.app.auth.config.AppAppleAuthProperties;
import com.tcm.training.admin.app.auth.dto.AppAppleLoginRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
/**
 * 苹果登录验签客户端,负责校验 identityToken 并提取最小身份标识。
 */
@Service
public class AppAppleAuthClient {
    private static final Logger log = LoggerFactory.getLogger(AppAppleAuthClient.class);
    private final AppAppleAuthProperties properties;
    private final ObjectMapper objectMapper;
    private final RestOperations restOperations;
    /**
     * 使用默认 HTTP 客户端构造苹果认证客户端。
     */
    @Autowired
    public AppAppleAuthClient(AppAppleAuthProperties properties, ObjectMapper objectMapper) {
        this(properties, objectMapper, new RestTemplate());
    }
    /**
     * 注入苹果配置、JSON 解析器与 HTTP 客户端。
     */
    AppAppleAuthClient(AppAppleAuthProperties properties,
                       ObjectMapper objectMapper,
                       RestOperations restOperations) {
        this.properties = properties;
        this.objectMapper = objectMapper;
        this.restOperations = restOperations;
    }
    /**
     * 校验苹果 identityToken 并提取本地登录所需的最小身份资料。
     */
    public AppAppleUserProfile resolveUserProfile(AppAppleLoginRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("苹果授权信息不完整");
        }
        if (!properties.isConfigured()) {
            throw new IllegalArgumentException("苹果登录暂未配置,请稍后重试");
        }
        String identityToken = trimToNull(request.getIdentityToken());
        String userIdentifier = trimToNull(request.getUserIdentifier());
        String authorizationCode = trimToNull(request.getAuthorizationCode());
        if (identityToken == null) {
            throw new IllegalArgumentException("苹果授权信息不完整");
        }
        SignedJWT signedJwt = parseSignedJwt(identityToken);
        verifySignature(signedJwt);
        JWTClaimsSet claimsSet = parseClaims(signedJwt);
        validateClaims(claimsSet);
        String subject = trimToNull(claimsSet.getSubject());
        if (subject == null) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
        if (userIdentifier != null && !userIdentifier.equals(subject)) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
        AppAppleUserProfile profile = new AppAppleUserProfile();
        profile.setSubject(subject);
        profile.setUserIdentifier(userIdentifier == null ? subject : userIdentifier);
        profile.setAuthorizationCode(authorizationCode);
        return profile;
    }
    /**
     * 解析苹果返回的 JWT。
     */
    private SignedJWT parseSignedJwt(String identityToken) {
        try {
            return SignedJWT.parse(identityToken);
        } catch (ParseException ex) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
    }
    /**
     * 校验苹果 JWT 签名。
     */
    private void verifySignature(SignedJWT signedJwt) {
        try {
            if (!JWSAlgorithm.RS256.equals(signedJwt.getHeader().getAlgorithm())) {
                throw new IllegalArgumentException("苹果登录授权失败");
            }
            String keyId = trimToNull(signedJwt.getHeader().getKeyID());
            if (keyId == null) {
                throw new IllegalArgumentException("苹果登录授权失败");
            }
            RSAKey rsaKey = loadRsaKey(keyId);
            if (!signedJwt.verify(new RSASSAVerifier(rsaKey.toRSAPublicKey()))) {
                throw new IllegalArgumentException("苹果登录授权失败");
            }
        } catch (JOSEException ex) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
    }
    /**
     * 读取并匹配苹果公钥。
     */
    private RSAKey loadRsaKey(String keyId) {
        JWKSet jwkSet = loadJwkSet();
        List<JWK> keys = jwkSet.getKeys();
        for (JWK key : keys) {
            if (!(key instanceof RSAKey)) {
                continue;
            }
            if (keyId.equals(key.getKeyID())) {
                return (RSAKey) key;
            }
        }
        throw new IllegalArgumentException("苹果登录授权失败");
    }
    /**
     * 拉取苹果 JWKS 公钥集合。
     */
    private JWKSet loadJwkSet() {
        try {
            String responseBody = restOperations.getForObject(properties.getJwksUrl(), String.class);
            if (responseBody == null || responseBody.trim().isEmpty()) {
                throw new IllegalArgumentException("苹果登录授权失败");
            }
            objectMapper.readTree(responseBody);
            return JWKSet.parse(responseBody);
        } catch (IllegalArgumentException ex) {
            throw ex;
        } catch (Exception ex) {
            log.error("拉取苹果 JWKS 失败: url={}", properties.getJwksUrl(), ex);
            throw new IllegalArgumentException("苹果登录授权失败");
        }
    }
    /**
     * 解析 JWT Claims。
     */
    private JWTClaimsSet parseClaims(SignedJWT signedJwt) {
        try {
            return signedJwt.getJWTClaimsSet();
        } catch (ParseException ex) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
    }
    /**
     * 校验 JWT Claims 是否满足本系统登录要求。
     */
    private void validateClaims(JWTClaimsSet claimsSet) {
        String issuer = trimToNull(claimsSet.getIssuer());
        if (!properties.getIssuer().equals(issuer)) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
        String audience = resolveAudience(claimsSet.getAudience());
        if (!properties.getClientId().equals(audience)) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
        Date expireAt = claimsSet.getExpirationTime();
        if (expireAt == null || expireAt.before(new Date())) {
            throw new IllegalArgumentException("苹果登录授权失败");
        }
    }
    /**
     * 读取 audience,当前仅接受单值 clientId。
     */
    private String resolveAudience(List<String> audienceList) {
        if (audienceList == null || audienceList.isEmpty()) {
            return null;
        }
        return trimToNull(audienceList.get(0));
    }
    /**
     * 将空白字符串归一化为 null。
     */
    private String trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String trimmed = value.trim();
        return trimmed.isEmpty() ? null : trimmed;
    }
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAppleUserProfile.java
New file
@@ -0,0 +1,18 @@
package com.tcm.training.admin.app.auth.service;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
 * 苹果登录解析后的最小身份资料。
 */
@Getter
@Setter
@NoArgsConstructor
public class AppAppleUserProfile {
    private String subject;
    private String userIdentifier;
    private String authorizationCode;
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/auth/service/AppAuthService.java
@@ -4,6 +4,7 @@
import com.tcm.training.admin.app.auth.dto.AppPasswordLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest;
import com.tcm.training.admin.app.auth.dto.AppAppleLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
import com.tcm.training.admin.app.auth.dto.AppSmsLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest;
@@ -39,6 +40,7 @@
    private final BCryptPasswordEncoder passwordEncoder;
    private final AppAuthSessionService appAuthSessionService;
    private final AppWechatAuthClient appWechatAuthClient;
    private final AppAppleAuthClient appAppleAuthClient;
    private final StringRedisTemplate stringRedisTemplate;
    /**
@@ -49,12 +51,14 @@
                          BCryptPasswordEncoder passwordEncoder,
                          AppAuthSessionService appAuthSessionService,
                          AppWechatAuthClient appWechatAuthClient,
                          AppAppleAuthClient appAppleAuthClient,
                          StringRedisTemplate stringRedisTemplate) {
        this.userMapper = userMapper;
        this.appSmsCodeService = appSmsCodeService;
        this.passwordEncoder = passwordEncoder;
        this.appAuthSessionService = appAuthSessionService;
        this.appWechatAuthClient = appWechatAuthClient;
        this.appAppleAuthClient = appAppleAuthClient;
        this.stringRedisTemplate = stringRedisTemplate;
    }
@@ -114,6 +118,25 @@
        requireEnabledUser(user);
        // 登录时同步微信专用资料,并仅在本地昵称头像为空时补齐展示字段。
        applyWechatProfile(user, profile, false);
        userMapper.updateById(user);
        return createLoginResponse(user);
    }
    /**
     * 使用苹果授权结果完成 APP 登录,首次登录自动创建普通用户。
     */
    public AppLoginResponse appleLogin(AppAppleLoginRequest request) {
        AppAppleUserProfile profile = appAppleAuthClient.resolveUserProfile(request);
        AppUser user = findUserByAppleSubject(profile.getSubject());
        if (user == null) {
            user = buildAppleUser(profile);
            userMapper.insert(user);
            return createLoginResponse(user);
        }
        requireEnabledUser(user);
        // 登录时同步苹果绑定标识,避免历史账号缺少最新 subject 或绑定时间。
        applyAppleProfile(user, profile, false);
        userMapper.updateById(user);
        return createLoginResponse(user);
    }
@@ -227,6 +250,16 @@
    }
    /**
     * 按苹果 subject 查询本地 APP 用户。
     */
    private AppUser findUserByAppleSubject(String subject) {
        return userMapper.selectOne(new LambdaQueryWrapper<AppUser>()
                .eq(AppUser::getAppleIdentitySubject, subject)
                .eq(AppUser::getDeleted, 0)
                .last("limit 1"));
    }
    /**
     * 基于微信资料构造首次登录时的本地普通用户。
     */
    private AppUser buildWechatUser(AppWechatUserProfile profile) {
@@ -234,6 +267,17 @@
        user.setType(0);
        user.setStatus(1);
        applyWechatProfile(user, profile, true);
        return user;
    }
    /**
     * 基于苹果资料构造首次登录时的本地普通用户。
     */
    private AppUser buildAppleUser(AppAppleUserProfile profile) {
        AppUser user = new AppUser();
        user.setType(0);
        user.setStatus(1);
        applyAppleProfile(user, profile, true);
        return user;
    }
@@ -261,6 +305,19 @@
    }
    /**
     * 将苹果资料回填到本地用户,并在首次自动建号时补齐基础展示昵称。
     */
    private void applyAppleProfile(AppUser user, AppAppleUserProfile profile, boolean syncLocalProfile) {
        user.setAppleUserIdentifier(trimToNull(profile.getUserIdentifier()));
        user.setAppleIdentitySubject(trimToNull(profile.getSubject()));
        user.setAppleBoundAt(LocalDateTime.now());
        if (syncLocalProfile || trimToNull(user.getNickname()) == null) {
            user.setNickname("苹果用户");
        }
    }
    /**
     * 将微信性别值转换为本地用户性别枚举。
     */
    private Integer resolveWechatGender(Integer sex) {
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/entity/AppUser.java
@@ -41,6 +41,15 @@
    @TableField("wechat_bound_at")
    private LocalDateTime wechatBoundAt;
    @TableField("apple_user_identifier")
    private String appleUserIdentifier;
    @TableField("apple_identity_subject")
    private String appleIdentitySubject;
    @TableField("apple_bound_at")
    private LocalDateTime appleBoundAt;
    /** 用户类型:0个人 1诊所 */
    private Integer type;
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/member/row/UserDetailRow.java
@@ -30,6 +30,9 @@
    private String wechatNickname;
    private String wechatAvatar;
    private LocalDateTime wechatBoundAt;
    private String appleUserIdentifier;
    private String appleIdentitySubject;
    private LocalDateTime appleBoundAt;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}
tcm-training-admin-backend/src/main/resources/application.yml
@@ -113,6 +113,13 @@
      user-info-url: https://api.weixin.qq.com/sns/userinfo
      # 微信授权换票接口地址
      access-token-url: https://api.weixin.qq.com/sns/oauth2/access_token
    apple:
      # 苹果登录 clientId,需与 iOS 包内 Sign in with Apple 配置保持一致
      client-id: "com.haizhentong.app"
      # 苹果 identity token 固定签发方
      issuer: https://appleid.apple.com
      # 苹果 JWKS 公钥地址
      jwks-url: https://appleid.apple.com/auth/keys
  share:
    # 邀请二维码默认指向线上下载页,开发环境可在 application-local.yml 覆盖。
    landing-base-url: https://haizhentong.com/app-download/
tcm-training-admin-backend/src/main/resources/mapper/UserMapper.xml
@@ -118,6 +118,9 @@
            s.wechat_nickname AS wechatNickname,
            s.wechat_avatar AS wechatAvatar,
            s.wechat_bound_at AS wechatBoundAt,
            s.apple_user_identifier AS appleUserIdentifier,
            s.apple_identity_subject AS appleIdentitySubject,
            s.apple_bound_at AS appleBoundAt,
            s.created_at AS createdAt,
            s.updated_at AS updatedAt
        FROM tcm_app_user s
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/auth/service/AppAuthServiceTest.java
@@ -5,6 +5,7 @@
import com.tcm.training.admin.app.auth.dto.AppPasswordLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetRequest;
import com.tcm.training.admin.app.auth.dto.AppPasswordResetVerifyRequest;
import com.tcm.training.admin.app.auth.dto.AppAppleLoginRequest;
import com.tcm.training.admin.app.auth.dto.AppSmsCodeSendRequest;
import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest;
import com.tcm.training.admin.app.auth.vo.AppLoginResponse;
@@ -68,6 +69,9 @@
    private AppWechatAuthClient appWechatAuthClient;
    @Mock
    private AppAppleAuthClient appAppleAuthClient;
    @Mock
    private StringRedisTemplate stringRedisTemplate;
    @Mock
@@ -89,6 +93,7 @@
                passwordEncoder,
                appAuthSessionService,
                appWechatAuthClient,
                appAppleAuthClient,
                stringRedisTemplate
        );
    }
@@ -295,6 +300,92 @@
    }
    /**
     * 苹果已绑定用户登录时应更新苹果绑定信息并返回统一登录响应。
     */
    @Test
    void appleLoginShouldReuseExistingUser() {
        AppAppleLoginRequest request = new AppAppleLoginRequest();
        request.setIdentityToken("identity-token");
        request.setUserIdentifier("apple-user-1");
        AppAppleUserProfile profile = new AppAppleUserProfile();
        profile.setSubject("apple-subject-1");
        profile.setUserIdentifier("apple-user-1");
        AppUser user = buildEnabledUser("13800138009", "ENCODED");
        user.setAppleIdentitySubject("apple-subject-1");
        user.setAppleUserIdentifier("apple-user-legacy");
        user.setNickname("已存在用户");
        SaTokenInfo tokenInfo = new SaTokenInfo();
        tokenInfo.setTokenValue("apple-token");
        tokenInfo.setTokenTimeout(7200L);
        when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile);
        when(userMapper.selectOne(any())).thenReturn(user);
        when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo);
        AppLoginResponse response = appAuthService.appleLogin(request);
        verify(userMapper).updateById(appUserCaptor.capture());
        verify(userMapper, never()).insert(any());
        assertEquals("apple-subject-1", appUserCaptor.getValue().getAppleIdentitySubject());
        assertEquals("apple-user-1", appUserCaptor.getValue().getAppleUserIdentifier());
        assertEquals("已存在用户", response.getUser().getNickname());
        assertEquals("apple-token", response.getToken());
    }
    /**
     * 苹果首次登录时应自动创建普通用户并等待后续绑定手机号。
     */
    @Test
    void appleLoginShouldCreateUserWhenSubjectNotExists() {
        AppAppleLoginRequest request = new AppAppleLoginRequest();
        request.setIdentityToken("identity-token");
        request.setUserIdentifier("apple-user-new");
        AppAppleUserProfile profile = new AppAppleUserProfile();
        profile.setSubject("apple-subject-new");
        profile.setUserIdentifier("apple-user-new");
        SaTokenInfo tokenInfo = new SaTokenInfo();
        tokenInfo.setTokenValue("apple-new-token");
        tokenInfo.setTokenTimeout(7200L);
        when(appAppleAuthClient.resolveUserProfile(request)).thenReturn(profile);
        when(userMapper.selectOne(any())).thenReturn(null);
        when(appAuthSessionService.getTokenInfo()).thenReturn(tokenInfo);
        AppLoginResponse response = appAuthService.appleLogin(request);
        verify(userMapper).insert(appUserCaptor.capture());
        verify(userMapper, never()).updateById(any());
        assertNull(appUserCaptor.getValue().getPhone());
        assertEquals("apple-subject-new", appUserCaptor.getValue().getAppleIdentitySubject());
        assertEquals("apple-user-new", appUserCaptor.getValue().getAppleUserIdentifier());
        assertEquals("苹果用户", appUserCaptor.getValue().getNickname());
        assertEquals(Integer.valueOf(0), appUserCaptor.getValue().getType());
        assertEquals("apple-new-token", response.getToken());
    }
    /**
     * 苹果授权无效时应直接拒绝登录。
     */
    @Test
    void appleLoginShouldFailWhenIdentityTokenInvalid() {
        AppAppleLoginRequest request = new AppAppleLoginRequest();
        request.setIdentityToken("bad-token");
        request.setUserIdentifier("apple-user-bad");
        when(appAppleAuthClient.resolveUserProfile(request)).thenThrow(new IllegalArgumentException("苹果登录授权失败"));
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> appAuthService.appleLogin(request));
        assertEquals("苹果登录授权失败", exception.getMessage());
    }
    /**
     * 统一构造启用中的 APP 用户测试桩。
     */
    private AppUser buildEnabledUser(String phone, String password) {
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/register/service/AppRegisterServiceTest.java
@@ -12,6 +12,7 @@
import com.tcm.training.admin.app.register.dto.AppRegisterUserRequest;
import com.tcm.training.admin.app.register.vo.AppRegisterOperatorPrepareResponse;
import com.tcm.training.admin.app.register.vo.AppRegisterResultVO;
import com.tcm.training.admin.app.share.service.AppShareService;
import com.tcm.training.admin.live.entity.Clinic;
import com.tcm.training.admin.live.mapper.ClinicMapper;
import com.tcm.training.admin.member.entity.AppUser;
@@ -88,7 +89,8 @@
    @Captor
    private ArgumentCaptor<AppUser> appUserCaptor;
    @Mock
    private AppShareService appShareService;
    /**
     * 使用 Mockito mock 重新组装被测服务。
     */
@@ -103,7 +105,8 @@
                hztAppUserMapper,
                hztClinicMapper,
                stringRedisTemplate,
                new ObjectMapper()
                new ObjectMapper(),
                appShareService
        );
    }
traditionalApp/manifest.json
@@ -74,6 +74,10 @@
                    }
                },
                "oauth": {
                    "apple": {
                        "__platform__": ["ios"],
                        "bundleId": "com.haizhentong.app"
                    },
                    "weixin": {
                        "appid": "wx074c1f0cf5a0ecf4",
                        "UniversalLinks": "https://static-mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8.next.bspapp.com/uni-universallinks/__UNI__1A90541/"
@@ -161,4 +165,4 @@
        }
    },
    "_spaceID": "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8"
}
}
traditionalApp/pages/login/login.vue
@@ -35,17 +35,23 @@
            <text class="forgot-password" v-if="loginType === 'password'" @tap="goForgotPassword">忘记密码?</text>
        </view>
        <view class="wechat-btn" @tap="handleWechatLogin">
            <image class="wechat-icon" src="/static/image/login/login@2x.png" mode="aspectFit"></image>
            <text class="wechat-text">微信登录</text>
        <view class="oauth-actions">
            <view class="oauth-btn apple-btn" v-if="showAppleLogin" @tap="handleAppleLogin">
                <text class="apple-icon"></text>
                <text class="oauth-text">Apple 登录</text>
            </view>
            <view class="oauth-btn wechat-btn" @tap="handleWechatLogin">
                <image class="wechat-icon" src="/static/image/login/login@2x.png" mode="aspectFit"></image>
                <text class="oauth-text">微信登录</text>
            </view>
        </view>
    </view>
</template>
<script setup lang="ts">
    import { onLoad } from '@dcloudio/uni-app'
    import { onUnmounted, ref } from 'vue'
    import { passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
    import { computed, onUnmounted, ref } from 'vue'
    import { appleLogin, passwordLogin, sendSmsCode, smsLogin, wechatLogin } from './service.js'
    const loginType = ref<'code' | 'password'>('code')
    const phone = ref('')
@@ -89,6 +95,12 @@
        unionid ?: string
    }
    interface AppAppleLoginPayload {
        identityToken : string
        authorizationCode ?: string
        userIdentifier ?: string
    }
    interface WechatAuthResult {
        access_token ?: string
        accessToken ?: string
@@ -102,6 +114,38 @@
        code ?: string
        authResult ?: WechatAuthResult
    }
    interface AppleAuthResult {
        identityToken ?: string
        authorizationCode ?: string
        user ?: string
        userIdentifier ?: string
        fullName ?: {
            nickname ?: string
            givenName ?: string
            familyName ?: string
        }
    }
    interface AppleLoginResult {
        authResult ?: AppleAuthResult
    }
    const showAppleLogin = computed(() => {
        // #ifdef APP-PLUS
        if (uni.getSystemInfoSync().platform !== 'ios') {
            return false
        }
        const system = normalizeOptionalText(uni.getSystemInfoSync().system)
        const versionText = system.replace(/^[^0-9]*/, '').split(' ')[0]
        const majorVersion = Number.parseInt(versionText.split('.')[0] || '0', 10)
        return majorVersion >= 13
        // #endif
        // #ifndef APP-PLUS
        return false
        // #endif
    })
    /**
     * 页面加载时记录来源参数,便于后续扩展回跳逻辑。
@@ -215,6 +259,59 @@
    }
    /**
     * 执行苹果登录,成功后复用现有登录态与绑手机号跳转逻辑。
     */
    const handleAppleLogin = async () => {
        if (loading.value) {
            return
        }
        // #ifdef APP-PLUS
        loading.value = true
        try {
            const loginRes = await requestAppleAuthorization()
            const payload = buildAppleLoginPayload(loginRes)
            if (!payload) {
                return
            }
            const res = await appleLogin(payload)
            if (!isSuccessResponse(res)) {
                return
            }
            saveLoginSession(res.data)
            if (shouldBindPhone(res.data)) {
                uni.redirectTo({
                    url: '/pages/bindPhone/bindPhone'
                })
                return
            }
            uni.showToast({
                title: '登录成功',
                icon: 'success'
            })
            setTimeout(() => {
                uni.switchTab({
                    url: '/pages/home/home'
                })
            }, 300)
        } catch (error: any) {
            uni.showToast({
                title: normalizeOptionalText(error?.errMsg) || '苹果登录失败',
                icon: 'none'
            })
        } finally {
            loading.value = false
        }
        // #endif
        // #ifndef APP-PLUS
        uni.showToast({
            title: '请在App中使用 Apple 登录',
            icon: 'none'
        })
        // #endif
    }
    /**
     * 拉起微信授权,获取后端换取用户资料所需的授权结果。
     */
    const requestWechatAuthorization = () => {
@@ -255,6 +352,68 @@
                    reject(error)
                }
            })
        })
    }
    /**
     * 拉起苹果授权,获取后端验签与建号所需的授权结果。
     */
    const requestAppleAuthorization = () => {
        return new Promise<AppleLoginResult>((resolve, reject) => {
            // #ifdef APP-PLUS
            plus.oauth.getServices((services) => {
                const appleService = services.find((service) => service.id === 'apple')
                if (!appleService) {
                    reject({
                        code: -3,
                        errMsg: '当前环境不支持 Apple 登录'
                    })
                    return
                }
                if (typeof appleService.login !== 'function') {
                    reject({
                        code: -4,
                        errMsg: '当前环境不支持 Apple 登录'
                    })
                    return
                }
                const executeAppleLogin = () => {
                    appleService.login((event : any) => {
                        const appleInfo = appleService.appleInfo || event?.appleInfo || {}
                        const authResult = appleService.authResult || event?.authResult || {}
                        resolve({
                            authResult: {
                                identityToken: appleInfo.identityToken || authResult.identityToken,
                                authorizationCode: appleInfo.authorizationCode || authResult.authorizationCode,
                                user: appleInfo.user || authResult.user,
                                userIdentifier: appleInfo.user || authResult.user
                            }
                        })
                    }, (error : any) => {
                        reject({
                            code: error?.code,
                            errMsg: error?.message || '苹果授权失败'
                        })
                    }, {
                        scope: 'email'
                    })
                }
                if (typeof appleService.logout === 'function') {
                    appleService.logout(() => {
                        executeAppleLogin()
                    }, () => {
                        executeAppleLogin()
                    })
                    return
                }
                executeAppleLogin()
            }, (error : any) => {
                reject({
                    code: error?.code,
                    errMsg: error?.message || '获取 Apple 登录服务失败'
                })
            })
            // #endif
        })
    }
@@ -336,6 +495,27 @@
            icon: 'none'
        })
        return null
    }
    /**
     * 从苹果授权结果中提取后端登录接口需要的参数。
     */
    const buildAppleLoginPayload = (loginRes : AppleLoginResult) => {
        const authResult = loginRes?.authResult || {}
        const identityToken = normalizeOptionalText(authResult.identityToken)
        if (!identityToken) {
            uni.showToast({
                title: '苹果授权信息缺失',
                icon: 'none'
            })
            return null
        }
        const payload : AppAppleLoginPayload = {
            identityToken,
            authorizationCode: normalizeOptionalText(authResult.authorizationCode),
            userIdentifier: normalizeOptionalText(authResult.userIdentifier || authResult.user)
        }
        return payload
    }
    /**
@@ -444,7 +624,7 @@
    }
    /**
     * 判断微信登录返回用户是否缺少手机号,缺少时进入绑定手机号流程。
     * 判断第三方登录返回用户是否缺少手机号,缺少时进入绑定手机号流程。
     */
    const shouldBindPhone = (loginData : AppLoginPayload) => {
        return !normalizeOptionalText(loginData?.user?.phone)
@@ -675,26 +855,54 @@
        }
    }
    .wechat-btn {
    .oauth-actions {
        width: 100%;
        margin-top: 100rpx;
        display: flex;
        flex-direction: column;
    }
    .oauth-btn {
        width: 400rpx;
        height: 88rpx;
        margin: 100rpx auto 0;
        margin: 0 auto;
        display: flex;
        align-items: center;
        justify-content: center;
        border: 2px solid #EEEEEE;
        border-radius: 16rpx;
        box-sizing: border-box;
        background-color: #FFFFFF;
        .oauth-text {
            font-size: 30rpx;
            color: #333333;
        }
    }
    .apple-btn {
        background-color: #111111;
        border-color: #111111;
        .apple-icon {
            font-size: 40rpx;
            line-height: 1;
            color: #FFFFFF;
            margin-right: 16rpx;
        }
        .oauth-text {
            color: #FFFFFF;
        }
    }
    .wechat-btn {
        margin-top: 24rpx;
        .wechat-icon {
            width: 56rpx;
            height: 56rpx;
            margin-right: 16rpx;
        }
        .wechat-text {
            font-size: 30rpx;
            color: #333333;
        }
    }
</style>
traditionalApp/pages/login/service.js
@@ -1,6 +1,13 @@
import request from '@/utils/request.js'
/**
 * 使用苹果授权结果完成 APP 登录。
 */
export const appleLogin = (data) => {
    return request.post('/api/app/auth/apple-login', data)
}
/**
 * 使用微信授权结果完成 APP 登录。
 */
export const wechatLogin = (data) => {