| | |
| | | <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> |
| New file |
| | |
| | | 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; |
| | |
| | | `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 '年龄', |
| | |
| | | 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); |
| | | |
| | | -- ---------------------------- |
| | | -- 会员价格配置 |
| | |
| | | |
| | | -- 用户 |
| | | 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` |
| | |
| | | |
| | | 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`, |
| | |
| | | `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 '年龄', |
| | |
| | | -- 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; |
| | | |
| | | -- ---------------------------- |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 使用苹果授权信息完成 APP 登录。 |
| | | */ |
| | | @PostMapping("/apple-login") |
| | | public ApiResponse<AppLoginResponse> appleLogin(@Valid @RequestBody AppAppleLoginRequest request) { |
| | | return ApiResponse.ok(appAuthService.appleLogin(request)); |
| | | } |
| | | |
| | | /** |
| | | * 发送 APP 忘记密码验证码。 |
| | | */ |
| | | @PostMapping("/password-reset/code") |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | |
| | | private final BCryptPasswordEncoder passwordEncoder; |
| | | private final AppAuthSessionService appAuthSessionService; |
| | | private final AppWechatAuthClient appWechatAuthClient; |
| | | private final AppAppleAuthClient appAppleAuthClient; |
| | | private final StringRedisTemplate stringRedisTemplate; |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 按苹果 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) { |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | /** |
| | | * 将苹果资料回填到本地用户,并在首次自动建号时补齐基础展示昵称。 |
| | | */ |
| | | 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) { |
| | |
| | | @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; |
| | | |
| | |
| | | private String wechatNickname; |
| | | private String wechatAvatar; |
| | | private LocalDateTime wechatBoundAt; |
| | | private String appleUserIdentifier; |
| | | private String appleIdentitySubject; |
| | | private LocalDateTime appleBoundAt; |
| | | private LocalDateTime createdAt; |
| | | private LocalDateTime updatedAt; |
| | | } |
| | |
| | | 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/ |
| | |
| | | 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 |
| | |
| | | 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; |
| | |
| | | private AppWechatAuthClient appWechatAuthClient; |
| | | |
| | | @Mock |
| | | private AppAppleAuthClient appAppleAuthClient; |
| | | |
| | | @Mock |
| | | private StringRedisTemplate stringRedisTemplate; |
| | | |
| | | @Mock |
| | |
| | | passwordEncoder, |
| | | appAuthSessionService, |
| | | appWechatAuthClient, |
| | | appAppleAuthClient, |
| | | stringRedisTemplate |
| | | ); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 苹果已绑定用户登录时应更新苹果绑定信息并返回统一登录响应。 |
| | | */ |
| | | @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) { |
| | |
| | | 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; |
| | |
| | | |
| | | @Captor |
| | | private ArgumentCaptor<AppUser> appUserCaptor; |
| | | |
| | | @Mock |
| | | private AppShareService appShareService; |
| | | /** |
| | | * 使用 Mockito mock 重新组装被测服务。 |
| | | */ |
| | |
| | | hztAppUserMapper, |
| | | hztClinicMapper, |
| | | stringRedisTemplate, |
| | | new ObjectMapper() |
| | | new ObjectMapper(), |
| | | appShareService |
| | | ); |
| | | } |
| | | |
| | |
| | | } |
| | | }, |
| | | "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/" |
| | |
| | | } |
| | | }, |
| | | "_spaceID": "mp-dd6d96de-bdf2-4284-a975-638af4cc8ec8" |
| | | } |
| | | } |
| | |
| | | <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('') |
| | |
| | | unionid ?: string |
| | | } |
| | | |
| | | interface AppAppleLoginPayload { |
| | | identityToken : string |
| | | authorizationCode ?: string |
| | | userIdentifier ?: string |
| | | } |
| | | |
| | | interface WechatAuthResult { |
| | | access_token ?: string |
| | | accessToken ?: string |
| | |
| | | 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 |
| | | }) |
| | | |
| | | /** |
| | | * 页面加载时记录来源参数,便于后续扩展回跳逻辑。 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 执行苹果登录,成功后复用现有登录态与绑手机号跳转逻辑。 |
| | | */ |
| | | 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 = () => { |
| | |
| | | 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 |
| | | }) |
| | | } |
| | | |
| | |
| | | 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 |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | /** |
| | | * 判断微信登录返回用户是否缺少手机号,缺少时进入绑定手机号流程。 |
| | | * 判断第三方登录返回用户是否缺少手机号,缺少时进入绑定手机号流程。 |
| | | */ |
| | | const shouldBindPhone = (loginData : AppLoginPayload) => { |
| | | return !normalizeOptionalText(loginData?.user?.phone) |
| | |
| | | } |
| | | } |
| | | |
| | | .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> |
| | |
| | | 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) => { |