文章目录
- 1准备
- 2流程分析
- [3 config层](#3 config层)
-
- [3.1 canal 监听mysql](#3.1 canal 监听mysql)
-
- [3.1.1 CanalClientConfig](#3.1.1 CanalClientConfig)
- [3.2 mybatisplus](#3.2 mybatisplus)
-
- [3.2.1 MybatisPlusConfig](#3.2.1 MybatisPlusConfig)
- [3.3 rabbitmq](#3.3 rabbitmq)
-
- [3.3.1 CanalRabbitMQConsumer消费队列](#3.3.1 CanalRabbitMQConsumer消费队列)
- [3.3.2 RabbitConfig创建队列](#3.3.2 RabbitConfig创建队列)
- [3.4 redis 配置序列化](#3.4 redis 配置序列化)
-
- [3.4.1 RedisConfig](#3.4.1 RedisConfig)
- [3.4.2 RedisUtils](#3.4.2 RedisUtils)
- [4 controller](#4 controller)
- [5 mapper](#5 mapper)
- [6 pojo](#6 pojo)
- [7 service](#7 service)
- [8 utils](#8 utils)
- [9 resources配置文件](#9 resources配置文件)
- [10 测试](#10 测试)
1准备
数据库准备
新建数据库smbms,在smbms中新建两张表
patient表
sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for patient
-- ----------------------------
DROP TABLE IF EXISTS `patient`;
CREATE TABLE `patient` (
`patient_id` bigint NOT NULL AUTO_INCREMENT COMMENT '患者唯一编号',
`patient_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '患者姓名',
`patient_type` tinyint NULL DEFAULT 1 COMMENT '患者分类(0: 临时, 1: 普通)',
`gender` int NOT NULL DEFAULT 2 COMMENT '性别(0: 未知, 1: 男, 2: 女)',
`priority_level` tinyint NULL DEFAULT 3 COMMENT '星级(客户重要程度,1-5)',
`age` int NULL DEFAULT NULL COMMENT '年龄',
`doctor_id` bigint NULL DEFAULT NULL COMMENT '主治医生ID(关联医生表)',
`channel_id` bigint NULL DEFAULT NULL COMMENT '渠道来源ID(关联渠道表)',
`birth_date` datetime NULL DEFAULT NULL COMMENT '出生日期,优先于年龄存储,更精确',
`patient_phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '手机号,用于登录和联系',
`patient_address` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '家庭地址',
`avatar_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '头像图片存储路径',
`remarks` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '患者备注(特殊情况说明)',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除(0:未删除, 1:已删除)',
`tenant_id` bigint NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`patient_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2145699 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '患者核心信息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of patient
-- ----------------------------
INSERT INTO `patient` VALUES (2145670, '周树立', 1, 2, 3, 24, 7, 4, '2001-06-13 00:00:00', '17737986892', '河南省洛阳市洛龙区盛唐至尊', '', '无', '', '2025-08-08 16:18:47', NULL, '2025-12-11 15:00:17', b'0', 1);
INSERT INTO `patient` VALUES (2145671, '张思雅', 0, 2, 3, 16, 1, 2, '2009-07-22 00:00:00', '13900139002', '上海市浦东新区张江路1500号', '/avatar/dental/1002.jpg', '牙列拥挤,需定期复诊调整', 'admin', '2025-11-24 10:49:33', 'admin', '2025-12-03 21:00:30', b'0', 1);
INSERT INTO `patient` VALUES (2145672, '晁锐', 1, 1, 3, 23, 7, 3, NULL, '18530729826', '', '', NULL, '1', '2025-12-02 15:49:31', '1', '2025-12-06 14:58:16', b'0', 1);
INSERT INTO `patient` VALUES (2145673, '张红', 1, 1, 3, 25, 1, 1, NULL, '15454568547', '', '', NULL, '1', '2025-12-03 14:17:16', '1', '2025-12-03 21:00:34', b'0', 1);
INSERT INTO `patient` VALUES (2145674, '周梳理', 1, 1, 3, 33, 1, 1, NULL, '15465452545', '', '', NULL, '1', '2025-12-03 14:53:31', '1', '2025-12-03 21:00:38', b'0', 1);
INSERT INTO `patient` VALUES (2145675, '陈建国', 1, 1, 1, 56, 1, 2, NULL, '15465897564', '', '', NULL, '1', '2025-12-03 21:03:39', '1', '2025-12-04 13:54:31', b'0', 1);
INSERT INTO `patient` VALUES (2145679, '周杰', 1, 1, 2, 19, 1, 3, '2005-12-07 08:00:00', '18454575458', '河南信阳市', '', NULL, '1', '2025-12-04 10:58:37', '1', '2025-12-04 11:48:12', b'0', 1);
INSERT INTO `patient` VALUES (2145680, '黄文隆', 1, 1, 2, 18, NULL, 2, '2006-12-05 08:00:00', '18454565245', '湖北武汉', '', NULL, '1', '2025-12-04 11:51:19', '1', '2025-12-04 11:51:19', b'0', 1);
INSERT INTO `patient` VALUES (2145681, '松鹤', 1, 1, 3, NULL, 7, 1, NULL, '18568986857', '', '', NULL, '1', '2025-12-04 14:32:35', '1', '2025-12-05 14:08:44', b'0', 1);
INSERT INTO `patient` VALUES (2145682, '赵正文', 1, 1, 2, 22, 19, 4, '2025-12-01 08:00:00', '17737986892', '北京三环四合院', '', NULL, '1', '2025-12-05 08:58:58', NULL, '2025-12-10 14:18:04', b'0', 2);
INSERT INTO `patient` VALUES (2145683, '王五', 1, 1, 3, NULL, 7, 3, NULL, '133333333333', '', '', NULL, '1', '2025-12-06 14:51:12', '1', '2025-12-06 14:51:12', b'0', 1);
INSERT INTO `patient` VALUES (2145684, '孙杨', 1, 1, 3, 25, 8, 4, NULL, '13333333333', '', '', NULL, '1', '2025-12-06 15:38:33', '1', '2025-12-06 15:38:33', b'0', 1);
INSERT INTO `patient` VALUES (2145685, '李明生', 1, 1, 1, 25, 32, 1, '2003-12-10 08:00:00', '18456545675', '湖北武汉', '', NULL, '1', '2025-12-08 09:41:24', '5', '2025-12-08 11:17:00', b'0', 6);
INSERT INTO `patient` VALUES (2145691, '周树里', 1, 2, 3, 25, 20, 3, '2001-09-01 09:59:04', '18568598568', '湖北武汉', '', NULL, '1', '2025-12-10 09:57:03', '1', '2025-12-10 10:01:51', b'0', 2);
INSERT INTO `patient` VALUES (2145693, '步鹏2', 1, 1, 3, 23, 32, 4, NULL, '13333333333', '', '', NULL, NULL, '2025-12-10 10:33:04', NULL, NULL, b'0', 6);
INSERT INTO `patient` VALUES (2145694, '树国辉', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, '2025-12-13 16:34:40', b'1', 0);
INSERT INTO `patient` VALUES (2145695, '步鹏5', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, NULL, b'0', 0);
INSERT INTO `patient` VALUES (2145696, '尤kangkang', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, NULL, b'0', 0);
INSERT INTO `patient` VALUES (2145697, '尤kangkang', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, NULL, b'0', 0);
INSERT INTO `patient` VALUES (2145698, '尤shuku', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, NULL, b'0', 0);
INSERT INTO `patient` VALUES (2145699, '白宁工', 1, 1, 3, NULL, NULL, NULL, NULL, '18456325647', '', '', NULL, NULL, NULL, NULL, '2025-12-13 21:59:22', b'1', 0);
SET FOREIGN_KEY_CHECKS = 1;
system_users表
sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for system_users
-- ----------------------------
DROP TABLE IF EXISTS `system_users`;
CREATE TABLE `system_users` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID(员工ID)',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码',
`nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`dept_id` bigint NULL DEFAULT NULL COMMENT '部门ID',
`post_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '岗位编号数组',
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户邮箱',
`mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '手机号码',
`sex` tinyint NULL DEFAULT 0 COMMENT '用户性别(0男1女2未知)',
`avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '头像地址(医生头像)',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '帐号状态(0正常 1停用)',
`login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '最后登录IP',
`login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号(诊所ID)',
`specialty` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '擅长领域',
`work_experience` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '工作经历',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Records of system_users
-- ----------------------------
INSERT INTO `system_users` VALUES (1, 'admin', '$2a$04$KljJDa/LK7QfDm0lF5OhuePhlPfjRH3tB2Wu351Uidz.oQGJXevPi', '牙智芯', '管理员', 103, '[1,2]', '11aoteman@126.com', '18438306487', 2, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/bn.png', 0, '192.168.108.28', '2025-12-11 20:53:26', 'admin', '2021-01-05 17:03:47', NULL, '2025-12-11 20:53:26', b'0', 1, '口腔全科诊疗、无痛舒适化治疗、龋病及非龋病的充填治疗、牙髓病的显微根管治疗,后牙嵌体及全冠修复、数字化口扫修复技术、瓷贴面修复、前牙美学树脂贴面修复微创无痛拔牙、牙周基础治疗、残根残冠及智齿微创拔除术、牙列缺损及缺失活动义齿修复、牙齿冷光美白。主张无痛舒适化治疗善于安抚恐惧看牙患者。追求精益求精、讲究更美观更舒适的患者就诊体验。', '毕业于郑州大学口腔医学系,从事口腔临床工作十余年。多次赴武汉大学口腔医学院学习进修。参加国内根管治疗、冠修复、前牙美学修复、种植修复等专业培训10余次。基本理论扎实,医疗技术娴熟,拥有较高水平的口腔综合诊疗能力。');
INSERT INTO `system_users` VALUES (2, 'zzz123', '$2a$04$5Jc7gGv1d2aRNuj7PTLDZO3hpwKQ/ueI0u39qeeIn/owUSVQlBN9y', '朱院长', '洛阳诊所管理员', NULL, NULL, '858255664@qq.com', '18888888888', 1, '', 0, '192.168.108.32', '2025-12-10 09:26:15', '1', '2025-12-03 17:04:27', NULL, '2025-12-10 09:26:15', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (3, 'hsh123', '$2a$04$EUU5sawqJXIgxNie/bN2j.OIQIlWEWmmuJ.8LQbc59hsjF6BTzvdW', '黄总', NULL, NULL, NULL, '', '18888888888', 0, '', 0, '', NULL, '1', '2025-12-03 17:09:23', '1', '2025-12-04 11:35:18', b'0', 3, NULL, NULL);
INSERT INTO `system_users` VALUES (4, 'lzh123', '$2a$04$9kwCGGTGQKcAXTnOGt5ZeurE4zWAF4iNd2QaopXWevs4id.DOgA0W', '卢总', NULL, NULL, NULL, '', '18888888888', 0, '', 0, '', NULL, '1', '2025-12-03 17:12:31', '1', '2025-12-04 11:35:13', b'0', 4, NULL, NULL);
INSERT INTO `system_users` VALUES (5, 'bn123', '$2a$04$bnWU1NtnZZ1yyAW3cRMPrOlY7mcbI4AzdUndT.02/ivh6t/3FGt5O', '白总', NULL, NULL, NULL, '', '18888888888', 1, '', 0, '192.168.42.1', '2025-12-08 11:23:12', '1', '2025-12-03 17:14:44', NULL, '2025-12-08 11:23:12', b'0', 6, NULL, NULL);
INSERT INTO `system_users` VALUES (6, 'lp123', '$2a$04$wLxM4NCw41XxPair6ehcdOpp5FdDLuKuhQ0p46pWcQPDEnoJJyL5e', '李品', NULL, NULL, NULL, '', '18888888888', 0, '', 0, '', NULL, '1', '2025-12-03 17:19:49', '1', '2025-12-04 11:35:05', b'0', 7, NULL, NULL);
INSERT INTO `system_users` VALUES (7, 'lzh123', '$2a$04$tkQMJgR0LUB/MSCAUKS9dO0qjkTpexhb2pJ1Uk3Iu22RCPyJYIXZa', '卢智豪', '牙科专家', 4, '[4]', '', '', 1, '\r\nhttps://yayixitong.oss-cn-beijing.aliyuncs.com/lzh.png', 0, '', NULL, '1', '2025-12-03 19:32:27', '1', '2025-12-09 11:43:22', b'0', 1, '儿童口腔疾病健康管理,儿童龋病的综合防治儿童牙体牙髓根尖周病的无痛微创诊疗,特别擅长儿童无痛舒适化口腔治疗、乳牙及恒牙的诊治,全麻下牙病治疗,儿童牙齿美容修复技术,儿童牙齿外伤的序列治疗,同时对儿童颜面畸形的早期诊断与预防矫治也积累了丰富的临床经验。', '从事儿童牙病临床研究15余年,经常前往国内外口腔界分享交流。多次担任口腔医学学生执业医生考试考官。\r\n2022年"华山杯"全国病例大赛一等奖');
INSERT INTO `system_users` VALUES (8, 'bn123', '$2a$04$TQPj8v0rZwnoamBlc2P0Q.R1fQft.Nu8O2Vc0CmZy0MyYtAat1ymq', '白宁', '牙科专家', 5, '[4]', '', '', 2, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/bn.png', 0, '192.168.42.1', '2025-12-05 15:08:30', '1', '2025-12-03 19:33:56', '1', '2025-12-09 11:50:43', b'0', 1, '专注种植、修复临床与研究20年,秉承无痛快速治疗理念,特别擅长各类阻生齿、复杂牙拔除、复杂根管治疗、各类固定及活动义齿修全口复杂义齿修复、前牙美学贴面,后牙复、嵌体微创修复。尤其擅长中老年人群高难度牙列治疗修复,精通国际前沿的活髓保存技术、断冠再接技术及前牙美容修复技术,DSD全数字化美学修复,3M-Lava全瓷冠修复,能最大化地恢复牙齿的美观及功能。', '从事口腔临床工作近20余年,少时曾赴国内外各类高级研修班学习,曾参加《专业种植AIC培训课程》德国Bumann教授《数字化咬合特训》以及北京口腔医学会组织的《现代显微根管治疗学及根尖病治疗技术》,为职业生涯奠定了扎实的基础理论知识并积累了丰富的临床工作经验。');
INSERT INTO `system_users` VALUES (9, 'hsh123', '$2a$04$wXWQHLmLzEBCXinhPGJFVe0k6PdpXzhaRWKw1XaPJuGH.HG5i8jia', '黄仕豪', '牙科主任', 8, '[4]', '', '', 1, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/hsh.png', 0, '', NULL, '1', '2025-12-03 19:55:14', '1', '2025-12-09 11:44:10', b'0', 1, '儿童牙病治疗,对儿童早期个性化预矫正有着独到见解。数字化隐形矫正技术、自锁托槽轻力矫治技术、青少年替牙期功能矫正技术、埋伏牙开窗牵引,成人正畸等各类复杂病例矫正。熟练掌握牙体、牙髓病治疗,多学科协作美学治疗及牙周常见病症治疗等口腔全科治疗项目,擅长阻生牙及各类复杂牙拔除,中、青年牙体缺损缺失的综合修复及仿生充填等。', '从事口腔临床工作多年,多次进修北大口腔有着良好的口腔理论基础和扎实的临床技术。');
INSERT INTO `system_users` VALUES (10, 'zjm123', '$2a$04$jADLeVPviGQOnQU7cvtIA.ZpEMctGmxCDA3TcujU2z0jy4.c1prWa', '张景明', '主任医师', 9, '[4,8]', '', '', 1, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/zjm.png', 0, '', NULL, '1', '2025-12-03 19:58:17', '1', '2025-12-09 11:48:17', b'0', 1, '全口及局部种植手术,即拔即种,微创无痛种植,数字化种植,种植固定全口修复、种植杆卡式活动全口修复,吸附性全口义齿修复,前牙高端全冠、瓷贴面美学修复等,口腔颌面部微创外科,微创无痛拔牙等', '毕业于原第三军医大学口腔医学系,从事口腔颌面外科种植修复30余年,二甲公立医院工作25年,曾进修第四军医大学郑大一附院各一年,发表论文十余篇。科技拔尖人才两届,带教进修生数十人。');
INSERT INTO `system_users` VALUES (11, 'lsm123', '$2a$04$MqalxL/ClUsx2A1J5TWDa.1BhzvIRyjUPMr9mnzkm/KN/DAs3e5jy', '李淑敏', '副主任医师', 14, '[4,8]', '', '', 2, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/lsm.png', 0, '', NULL, '1', '2025-12-03 19:58:46', '1', '2025-12-09 11:44:32', b'0', 1, '擅长乳牙中、深龋治疗术及新生恒牙预防性充填术,儿童各类乳牙滞留及多生牙快速无痛拔除术,龋病、牙髓病的无痛舒适化治疗,大面积龋齿的保守修复,前牙外伤的诊断治疗及前牙透明冠美容修复,以及牙周基础治疗等,针对儿童患牙的治疗及心理疏导有独特的见解和处理方式,对于高度不配合患儿的牙病治疗有着丰富的临床经验。', '师从第四军医大学口腔专业博士,专注儿童牙病临床10余年,先后于北京大学口腔医院牙体牙髓科、牙周科和修复科进修学习,曾在北大口腔医院组织的《牙冠延长术高级实操班》《牙周病学规范化诊疗技术培训班》中都取得异成绩。并积极参加黄华教授"儿童咬合诱导技术培训班"。工作认真、态度严谨、待人真诚、考虑周到,现已帮助数万名口腔患者解决了口腔问题,受到一致好评');
INSERT INTO `system_users` VALUES (12, 'why123', '$2a$04$hpxx03L4gHYuYktGirkBl.qeMAeQ7d.ZZvLoGS56V7Z8yj6l080L6', '王浩宇', '主治医师', 11, '[4]', '', '', 2, 'https://yayixitong.oss-cn-beijing.aliyuncs.com/why.png', 0, '', NULL, '1', '2025-12-03 19:59:34', '1', '2025-12-09 11:46:47', b'0', 1, '儿童口腔龋病诊断及治疗,牙体牙髓治疗,根尖周病的治疗,牙外伤诊断及治疗,儿童早期矫治以及儿童恐惧心理扭转、无痛舒适化就诊引导,临床治疗经验丰富,技术熟练精湛,耐心温柔,深受家长和小朋友的喜欢。', '毕业于新乡医学院,曾在三甲医院口腔科深造,多次参与北京大学口腔科医院课程培训,受邀参加多次儿童口腔技术新进展论坛峰会,致力于儿童牙病预防与治疗10余年。');
INSERT INTO `system_users` VALUES (13, 'cyt123', '$2a$04$NXhuPD4ekXDrRBgDIrDJxu6fua/WSuwtqirgI9exgtfESxJVxSgj.', '陈雨婷', '主治医师', 13, '[7]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:00:13', '1', '2025-12-04 11:38:31', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (14, 'zwg123', '$2a$04$Y20wKgmdXoXzzKr2quN6junWb7ig0HaF2O/ekHFNmXr8cmv2pjd22', '赵卫国', '主任医师', 15, '[4,8]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:00:42', '1', '2025-12-04 11:38:31', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (15, 'lf123', '$2a$04$vMelyFJv6B8FPy5slp.8N.5UNnyhbtttWFqsa2vWOJbGZzqEDJsby', '刘芳', '住院医师', 17, '[7]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:02:46', '1', '2025-12-04 11:38:33', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (16, 'zmx123', '$2a$04$rxX7MPzc8aziE1BWwcw0vexfrrJ3TfSXm5E3knDiMEpFhGBQNL/fa', '周明轩', '库房管理员', 18, '[7]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:06:50', '1', '2025-12-04 11:38:34', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (17, 'wx123', '$2a$04$trbF79gZJwGk6dIL37mJxO3VwdNVAz/fTZUOqtOpCMOWwA9Ubocom', '吴雪', '前台主管', 20, '[5,9]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:08:33', '1', '2025-12-04 11:38:35', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (18, 'sxy123', '$2a$04$EGvJFHBEh4k0fbXcIoLBn.cM1AvN8FTp5Ho7.cgQZleGkaztBQT5i', '孙晓雅', '诊疗护士', 4, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:10:52', '1', '2025-12-04 11:38:36', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (19, 'mc123', '$2a$04$aZMVrPRl7/LtAo7LP3ISA.gNBvjUXyiD/e07v1VL19DxCM74SZoYm', '马超', '诊疗护士', 5, '[6]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:11:16', '1', '2025-12-04 11:38:37', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (20, 'zl123', '$2a$04$BE7WLVyQyH0JXFYykZlqQOSdcXc6ERB9ycmg878pAMQVtektvQp9m', '朱琳', '诊疗护士', 8, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:11:40', '1', '2025-12-04 11:38:41', b'0', 2, NULL, NULL);
INSERT INTO `system_users` VALUES (21, 'lq123', '$2a$04$RRrbKNPXcjTzsUeBSo4aYOL5i1P3I.QQahvs6kyGj.SadCm6c6fR2', '林巧', '诊疗护士', 9, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:14:54', '1', '2025-12-03 20:14:54', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (22, 'zt123', '$2a$04$xIVjHq7XqAMWB3gpSPQRnOs7Wox35NRhvtKbBSUvCgyoDRsPwh10O', '郑涛', '消毒护士', 16, '[6]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:15:23', '1', '2025-12-03 20:15:36', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (23, 'ty123', '$2a$04$NNou8p5b.9VnwGpDCQ.uwu6yEaNi.PxBwnl0d93SQsIE5YT9rJA5S', '田雅', '药房护士', 17, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:16:05', '1', '2025-12-03 20:16:05', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (24, 'tt123', '$2a$04$7xSpFdaeJ948uu/HJSlFuOokoAnrojjOruQIKeJUKdeRzRMCuYkVC', '田甜', '诊疗护士', 15, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:19:12', '1', '2025-12-03 20:19:12', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (25, 'xj123', '$2a$04$o5Vga.8w8eE4xJZ4SKPMyOzGnmNjMlGDHbfJ05HW6n37rHSAG1tK6', '徐静', '后勤大妈', 19, '[7]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:21:50', '1', '2025-12-03 20:22:42', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (26, 'gy123', '$2a$04$eAGl8S.GozwUd0HYg3rbn.M0sODTULjY1vl8BqLwRDauR4ODfJL92', '郭阳', '前台接待', 20, '[5,9]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:22:23', '1', '2025-12-03 20:26:40', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (27, 'gm123', '$2a$04$7S3nje6tNgX8sl5Nz4YsFeNdb/nsRDIEgMf3XsWWgahskQp.uSVOC', '高敏', '财务结算员', 20, '[5,9]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:27:07', '1', '2025-12-03 20:27:22', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (28, 'zx123', '$2a$04$00lxPA2NQ6FSOgNa1sDkf.U0xJGJoE2TnhtOBv6Of44eOtiEYcJMu', '钟欣', '主治医师', 10, '[4]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:30:22', '1', '2025-12-03 20:30:22', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (29, 'wl123', '$2a$04$3AqZu531/MGt4g3dyDGkWuSD4akk0zwAeoJ.94zkY7/9IDNcZJYii', '王磊', '诊疗护士', 11, '[6]', '', '', 1, '', 0, '', NULL, '1', '2025-12-03 20:30:57', '1', '2025-12-03 20:30:57', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (30, 'fq123', '$2a$04$K6YXM8uWYokZJ/qGOOywKetrZ3E0Akt0Tpzg0zyq6Nb/vHydRtZyq', '方琦', '诊疗护士', 10, '[6]', '', '', 2, '', 0, '', NULL, '1', '2025-12-03 20:31:34', '1', '2025-12-03 20:31:34', b'0', 1, NULL, NULL);
INSERT INTO `system_users` VALUES (31, 'wujunbo', '$2a$04$p3.h7vIeC.a0u7nqbMcGW.F2soZrWHmSrxqVvAVdJZaL2Wi.RG9nG', '吴俊伯', '咨询师', 11, '[5]', '', '18456465457', 1, '', 0, '', NULL, '1', '2025-12-07 20:54:20', '1', '2025-12-07 20:54:20', b'0', 1, '咨询沟通', '三年工作经历');
INSERT INTO `system_users` VALUES (32, 'lsh123', '$2a$04$zySaR5Q8RQQW0bV1g7hHzumgFoU9mPnSvKkxUKoAoxGJ9Tl9.UYOm', '李思涵', NULL, 23, '[]', '', '18456545245', 2, '', 0, '', NULL, '1', '2025-12-08 09:56:07', '1', '2025-12-08 09:56:07', b'0', 6, NULL, NULL);
SET FOREIGN_KEY_CHECKS = 1;
新建项目
新建项目

引入依赖

引入依赖
xml
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Canal 客户端依赖 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.6</version>
</dependency>
<!-- Canal 协议依赖 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.protocol</artifactId>
<version>1.1.6</version>
</dependency>
<!-- Canal 公共组件 -->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.common</artifactId>
<version>1.1.6</version>
</dependency>
<!-- 消息队列 AMQP 支持(如 RabbitMQ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--添加 Jackson 处理 Java 8 日期时间的依赖:-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
修改配置文件
yml
server:
port: 8080
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/smbms?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
# mybatis-plus 相关配置
mybatis-plus:
mapper-locations: classpath:mappers/*.xml # 确保XML文件在此路径下
type-aliases-package: com.hsh.canal02.pojo #类型别名所在的包
#控制台打印sql语句
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #关闭驼峰映射
global-config:
db-config:
logic-delete-field: deleted #全局逻辑删除字段值 3.3.0开始支持,详情看下面。
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# Canal客户端配置(监听数据库binlog)
canal:
client:
instances:
example: # 实例名,可自定义
host: 192.168.14.254 # Canal Server地址
port: 11111 # Canal默认端口
username: canal # Canal默认用户名
password: canal # Canal默认密码
# 监听 smbms 库下的 smbms_provider 和 smbms_bill 表(多个表用逗号分隔)
filter: smbms.patient
# 日志配置
logging:
level:
org.springframework.web: INFO
com.github.binarywang.demo.wx.mp: DEBUG
me.chanjar.weixin: DEBUG
com.alibaba.otter.canal: INFO # 开启Canal日志,方便调试
#rabbitmq配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
2流程分析
我们canal1.1.8+mysql8.0+jdk17+rabbitMQ+redis的使用流程如下图所示

主要涉及两个类CanalClientConfig和CanalRabbitMQConsumer
下面我把代码复制过来看
3 config层
3.1 canal 监听mysql
3.1.1 CanalClientConfig
java
package com.hsh.canal02.config.canal;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Configuration
public class CanalClientConfig {
@Value("${canal.client.instances.example.host}")
private String canalHost;
@Value("${canal.client.instances.example.port}")
private int canalPort;
@Value("${canal.client.instances.example.username}")
private String canalUsername;
@Value("${canal.client.instances.example.password}")
private String canalPassword;
@Value("${canal.client.instances.example.filter}")
private String filter;
@Autowired
private RabbitTemplate rabbitTemplate;
// 项目启动时初始化Canal连接并开始监听
@PostConstruct
public void startCanalListener() {
// 创建Canal连接,并声明为final
final CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress(canalHost, canalPort),
"example", // 对应Canal服务的实例名(默认是example)
canalUsername,
canalPassword
);
// 启动监听线程(使用匿名内部类,兼容Java 5+)
new Thread(new Runnable() {
public void run() {
try {
connector.connect(); // 此处访问外部final变量
connector.subscribe(filter);
connector.rollback();
while (true) {
Message message = connector.getWithoutAck(1024); // 访问 外部final变量
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
} else {
handleMessage(message.getEntries());
connector.ack(batchId); // 访问外部final变量
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
connector.disconnect(); // 访问外部final变量
}
}
}).start();
}
// Canal 模拟 MySQL Slave 的交互协议,解析 MySQL binlog 后,
// 会将每条数据变更封装为 Entry 对象(对应你输出的整体内容),
// 核心分为 header(头信息)、entryType(条目类型)、storeValue(变更数据体)三部分。
private void handleMessage(List<CanalEntry.Entry> entries) {
for (CanalEntry.Entry entry : entries) {
// 只处理行数据变更
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
try {
// String jsonData = entry.toString();
// rabbitTemplate.convertAndSend("canal.exchange", "",
// jsonData);
// System.out.println("Canal监听变更,发送到RabbitMQ: " +
// jsonData);
// 解析storeValue为RowChange(核心:把二进制转成结构化对象)
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
// 1. 获取基础信息
String tableName = entry.getHeader().getTableName(); // 表名:patient
CanalEntry.EventType eventType = rowChange.getEventType(); // 操作类型:UPDATE
// 2. 遍历变更的行数据
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 2.1 变更前的数据(UPDATE才有,INSERT为null,DELETE只有before)
Map<String, String> beforeData = parseColumns(rowData.getBeforeColumnsList());
// 2.2 变更后的数据(UPDATE/INSERT有,DELETE为null)
Map<String, String> afterData = parseColumns(rowData.getAfterColumnsList());
// 3. 封装成可读的JSON(发送到RabbitMQ)
Map<String, Object> msg = new HashMap<>();
msg.put("table", tableName);
msg.put("eventType", eventType.name());
msg.put("before", beforeData); // 更新前的字段值
msg.put("after", afterData); // 更新后的字段值
// 发送到RabbitMQ(转成JSON字符串)
String jsonMsg = new ObjectMapper().writeValueAsString(msg);
rabbitTemplate.convertAndSend("canal.exchange", "", jsonMsg);
System.out.println("发送到RabbitMQ的可读消息:" + jsonMsg);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
// 辅助方法:把Canal的Column列表转成Map(字段名→字段值)
private Map<String, String> parseColumns(List<CanalEntry.Column> columns) {
Map<String, String> result = new HashMap<>();
if (columns == null || columns.isEmpty()) {
return result;
}
for (CanalEntry.Column column : columns) {
result.put(column.getName(), column.getValue()); // 字段名:值
}
return result;
}
}
3.2 mybatisplus
3.2.1 MybatisPlusConfig
java
package com.hsh.canal02.config.mybatisplus;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author xrkhy
* @date 2025/12/13 14:59
* @description
*/
@Configuration
@MapperScan("com.hsh.canal02.mapper")
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加MySQL分页拦截器(根据数据库类型选择,如Oracle、PostgreSQL等)
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
3.3 rabbitmq
3.3.1 CanalRabbitMQConsumer消费队列
java
package com.hsh.canal02.config.rabbitmq;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hsh.canal02.pojo.PatientDO;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Iterator;
import java.util.Map;
@Component
public class CanalRabbitMQConsumer {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
@RabbitListener(queues = "canal.sync.queue")
public void processCanalMessage(String message) {
try {
System.out.println("收到CANAL数据: " + message);
// 解析Canal消息
JsonNode rootNode = objectMapper.readTree(message);
String tableName = rootNode.get("table").asText();
String eventType = rootNode.get("eventType").asText();
// 只处理patient表的变更
if ("patient".equals(tableName)) {
handlePatientChange(eventType, rootNode);
}
// 存储最后变更记录
redisTemplate.opsForValue().set("canal:lastChange", message);
System.out.println("已同步到Redis: " + message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理患者表数据变更
*/
private void handlePatientChange(String eventType, JsonNode rootNode) throws Exception {
// 获取患者ID(新增/更新取after数据,删除取before数据)
JsonNode dataNode = "DELETE".equals(eventType) ?
rootNode.get("before") : rootNode.get("after");
Long patientId = dataNode.get("patient_id").asLong();
String cacheKey = "patients::id_" + patientId;
switch (eventType) {
case "INSERT":
case "UPDATE":
// 新增或更新时,将数据转为PatientDO并缓存
PatientDO patient = convertToPatientDO(dataNode);
redisTemplate.opsForValue().set(cacheKey, patient);
System.out.println("缓存更新成功: " + cacheKey);
break;
case "DELETE":
// 删除时,清除对应缓存
redisTemplate.delete(cacheKey);
System.out.println("缓存删除成功: " + cacheKey);
break;
default:
System.out.println("不处理的操作类型: " + eventType);
}
// 清除分页缓存(因为数据变更可能影响分页结果)
clearPatientPageCache();
}
/**
* 将JSON节点转换为PatientDO对象
*/
private PatientDO convertToPatientDO(JsonNode dataNode) throws Exception {
PatientDO patient = new PatientDO();
patient.setPatientId(dataNode.get("patient_id").asLong());
patient.setPatientName(dataNode.get("patient_name").asText());
patient.setPatientType(dataNode.has("patient_type") ? dataNode.get("patient_type").asInt() : null);
patient.setGender(dataNode.has("gender") ? dataNode.get("gender").asInt() : null);
patient.setAge(dataNode.has("age") ? dataNode.get("age").asInt() : null);
patient.setPatientPhone(dataNode.has("patient_phone") ? dataNode.get("patient_phone").asText() : null);
// 其他字段同理...
return patient;
}
/**
* 清除患者分页缓存(简单实现:删除所有分页相关缓存)
*/
private void clearPatientPageCache() {
// 实际项目中建议使用Redis的key前缀匹配删除
// 这里简化处理,生产环境建议使用更高效的方式
Iterator<Object> keys = redisTemplate.keys("patients::page_*").iterator();
while (keys.hasNext()) {
redisTemplate.delete(keys.next());
}
System.out.println("患者分页缓存已清除");
}
}
3.3.2 RabbitConfig创建队列
java
package com.hsh.canal02.config.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 1. 配置ObjectMapper(支持Java8时间类型+泛型类型检测)
ObjectMapper objectMapper = new ObjectMapper();
// 注册JavaTimeModule解决LocalDateTime序列化问题
objectMapper.registerModule(new JavaTimeModule());
// 关闭时间戳序列化(用ISO格式)
objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 开启泛型类型检测(关键:解决List/PageResult/IPage反序列化)
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL // 非final类都保留类型信息
);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 2. 通用Jackson序列化器(支持所有对象,包括单对象+分页泛型)
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jsonSerializer.setObjectMapper(objectMapper);
// 3. 配置序列化器(Key用String,Value用Jackson)
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
3.4 redis 配置序列化
3.4.1 RedisConfig
java
package com.hsh.canal02.config.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 1. 配置ObjectMapper(支持Java8时间类型+泛型类型检测)
ObjectMapper objectMapper = new ObjectMapper();
// 注册JavaTimeModule解决LocalDateTime序列化问题
objectMapper.registerModule(new JavaTimeModule());
// 关闭时间戳序列化(用ISO格式)
objectMapper.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 开启泛型类型检测(关键:解决List/PageResult/IPage反序列化)
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL // 非final类都保留类型信息
);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 2. 通用Jackson序列化器(支持所有对象,包括单对象+分页泛型)
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jsonSerializer.setObjectMapper(objectMapper);
// 3. 配置序列化器(Key用String,Value用Jackson)
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
3.4.2 RedisUtils
java
package com.hsh.canal02.config.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
/**
* Redis工具类,整合RedisTemplate和StringRedisTemplate
* 提供针对字符串和通用对象的全面操作
*/
@Component
public class RedisUtils {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
public String getStr(String key) {
return stringRedisTemplate.opsForValue().get(key);
}
public long getIncrement(String key) {
return stringRedisTemplate.opsForValue().increment(key);
}
/**
* 存储对象
* @param key Redis键
* @param value 存储的对象
*/
public void setObj(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
}
4 controller
PatientController
java
package com.hsh.canal02.controller.patient;
import com.hsh.canal02.controller.patient.patient.PatientPageReqVO;
import com.hsh.canal02.pojo.PatientDO;
import com.hsh.canal02.pojo.dto.ResultJSON;
import com.hsh.canal02.service.PatientService;
import com.hsh.canal02.utils.PageResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
@RestController
@RequestMapping("/patient/manage")
@Validated
@PermitAll
public class PatientController {
@Resource
private PatientService patientService;
@GetMapping("/page")
public ResultJSON<PageResult<PatientDO>> getUserPage(PatientPageReqVO pageReqVO) {
// 获得用户分页列表
PageResult<PatientDO> pageResult = patientService.getPatientPage(pageReqVO);
return ResultJSON.success(pageResult);
}
@GetMapping("/get")
@PermitAll
public ResultJSON<PatientDO> getUser(@RequestParam("id") Long id) {
PatientDO patientDO = patientService.getPatient(id);
if (patientDO == null) {
return ResultJSON.error(500,"用户不存在");
}
return ResultJSON.success(patientDO);
}
@PostMapping("/create")
public ResultJSON<Long> createUser( @RequestBody PatientDO patientDO) {
Integer id = patientService.createPatient(patientDO);
if (id == 0){
return ResultJSON.error(500,"创建用户失败");
}
// 返回用户编号
Long idLong = Long.valueOf(id);
return ResultJSON.success(idLong);
}
@PutMapping("/update")
public ResultJSON<Boolean> updateUser(@RequestBody PatientDO patientDO) {
patientService.updatePatient(patientDO);
return ResultJSON.success(true);
}
@DeleteMapping("/delete")
public ResultJSON<Boolean> deleteUser(@RequestParam("id") Long id) {
patientService.deletePatient(id);
return ResultJSON.success(true);
}
}
patient
PatientPageReqVO
java
package com.hsh.canal02.controller.patient.patient;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author xrkhy
* @date 2025/11/23 14:45
* @description
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PatientPageReqVO {
private static final Integer PAGE_NO = 1;
private static final Integer PAGE_SIZE = 10;
private Long patientId;
private String patientName;
private Integer patientType;
private Integer gender;
private Integer priorityLevel;
private Integer age;
private Long doctorId;
private Long channelId;
private LocalDate[] birthDate;
private String patientPhone;
private String patientAddress;
private String avatarUrl;
private String remarks;
// 创建的开始时间
private LocalDateTime[] createTime;
private String[] transformCreateTime;
/**
* 每页条数 - 不分页
*
* 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
*/
public static final Integer PAGE_SIZE_NONE = -1;
private Integer pageNo = PAGE_NO;
private Integer pageSize = PAGE_SIZE;
}
PatientRespVO
java
package com.hsh.canal02.controller.patient.patient;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* @author xrkhy
* @date 2025/11/23 14:44
* @description
*/
@Data
public class PatientRespVO {
private String patientID;
private String patientName;
private Integer patientType;
private Integer gender;
private Integer priorityLevel;
private Integer age;
private Long doctorId;
private Long channelId;
private LocalDate[] birthDate;
private String patientPhone;
private String patientAddress;
private String avatarUrl;
private String remarks;
private LocalDateTime createTime;
}
5 mapper
PatientMapper
java
package com.hsh.canal02.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.hsh.canal02.controller.patient.patient.PatientPageReqVO;
import com.hsh.canal02.pojo.PatientDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Collection;
import java.util.List;
/**
* @author xrkhy
* @date 2025/11/23 15:49
* @description
*/
@Mapper
public interface PatientMapper extends BaseMapper<PatientDO> {
IPage<PatientDO> selectMultipleTablePage(IPage<PatientDO> page, @Param("query") PatientPageReqVO query);
// 患者
}
6 pojo
AdminUserDO
java
package com.hsh.canal02.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Set;
/**
* 管理后台的用户 DO
*
* @author 芋道源码
*/
@TableName(value = "system_users", autoResultMap = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdminUserDO implements Serializable {
/**
* 用户ID
*/
@TableId
private Long id;
/**
* 用户账号
*/
private String username;
/**
* 加密后的密码
*
* */
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 备注
*/
private String remark;
/**
* 部门 ID
*/
private Long deptId;
/**
* 岗位编号数组
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private Set<Long> postIds;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String mobile;
/**
* 用户性别
**/
private Integer sex;
/**
* 用户头像
*/
private String avatar;
/**
* 帐号状态
**/
private Integer status;
/**
* 最后登录IP
*/
private String loginIp;
/**
* 最后登录时间
*/
private LocalDateTime loginDate;
/**
* 多租户编号
*/
private Long tenantId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除
*/
@TableLogic
private Boolean deleted;
}
PatientDO
java
package com.hsh.canal02.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.*;
import org.apache.ibatis.type.JdbcType;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
/**
* @author xrkhy
* @date 2025/11/23 15:47
* @description
*/
@TableName("patient")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PatientDO implements Serializable {
/**
* 患者唯一编号
*/
@TableId(type = IdType.AUTO)
private Long patientId;
/**
* 患者姓名
*/
private String patientName;
/**
* 患者分类(0: 临时, 1: 普通)
*/
private Integer patientType;
/**
* 性别(0: 未知, 1: 男, 2: 女)
*/
private Integer gender;
/**
* 星级(客户重要程度,1-5)
*/
private Integer priorityLevel;
/**
* 年龄
*/
private Integer age;
/**
* 主治医生ID(关联医生表)
*/
private Long doctorId;
/**
* 渠道来源ID(关联渠道表)
*/
private Long channelId;
/**
* 出生日期,优先于年龄存储,更精确
*/
// @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
@DateTimeFormat(pattern="yyyy-MM-dd")// 用户对象属性,控制入参时日期类型转换
@JsonFormat(pattern="yyyy-MM-dd")// 返回JSON数据时日期类型处理
private Date birthDate;
/**
* 手机号,用于登录和联系
*/
private String patientPhone;
/**
* 家庭地址
*/
private String patientAddress;
/**
* 头像图片存储路径
*/
private String avatarUrl;
/**
* 患者备注(特殊情况说明)
*/
private String remarks;
// 忽略字段
@TableField(exist = false)
private AdminUserDO user;
/**
* 多租户编号
*/
private Long tenantId;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 最后更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
@TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
private String updater;
/**
* 是否删除
*/
@TableLogic
private Boolean deleted;
}
dto
ResultJSON
java
package com.hsh.canal02.pojo.dto;
import java.io.Serializable;
/**
* @Author: wzy
* @Date: 2024/11/13 11:03
* @Description: 返回结果类
*/
public class ResultJSON<T> implements Serializable {
private Integer code;
private String msg;
private T data;
public ResultJSON(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
/**
* 操作成功或者失败
* @param c 受影响行数
* @return 当前传入的受影响行数>0则返回成功,否则返回失败
*/
public static ResultJSON successORerror(int c){
return c>0?new ResultJSON(200,"操作成功",c)
:new ResultJSON(400,"操作失败",c);
}
public static ResultJSON success(){
return new ResultJSON(200,"操作成功",null);
}
public static ResultJSON success(String msg){
return new ResultJSON(200,msg,null);
}
public static <T> ResultJSON success(T data){
return new ResultJSON(200,"操作成功",data);
}
public static ResultJSON success(Integer code,String msg){
return new ResultJSON(code,msg,null);
}
public static <T> ResultJSON success(String msg,T data){
return new ResultJSON(200,msg,data);
}
public static <T> ResultJSON success(Integer code,String msg,T data){
return new ResultJSON(code,msg,data);
}
public static ResultJSON error(){
return new ResultJSON(500,"操作失败",null);
}
public static ResultJSON error(String msg){
return new ResultJSON(500,msg,null);
}
public static ResultJSON error(Integer code,String msg){
return new ResultJSON(code,msg,null);
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
7 service
PatientService
java
package com.hsh.canal02.service;
import com.hsh.canal02.controller.patient.patient.PatientPageReqVO;
import com.hsh.canal02.pojo.PatientDO;
import com.hsh.canal02.pojo.dto.ResultJSON;
import com.hsh.canal02.utils.PageResult;
import java.util.List;
/**
* @author xrkhy
* @date 2025/11/23 15:58
* @description
*/
public interface PatientService {
/**
* 获得患者分页
*
* @param reqVO 患者分页查询
* @return 患者分页结果
*/
PageResult<PatientDO> getPatientPage(PatientPageReqVO reqVO);
/**
* 创建患者
*
* @param PatientDO 创建患者信息
* @return 患者编号
*/
Integer createPatient(PatientDO PatientDO);
/**
* 更新患者
*
* @param updateReqVO 更新患者信息
*/
void updatePatient(PatientDO updateReqVO);
/**
* 删除患者
*
* @param id 患者编号
*/
void deletePatient(Long id);
/**
* 批量删除患者
*
* @param ids 患者编号数组
*/
void deletePatientList(List<Long> ids);
/**
* 获得患者
*
* @param id 患者编号
* @return 患者
*/
PatientDO getPatient(Long id);
List<PatientDO> getAllPatients (PatientDO patientDO);
}
PatientServiceImpl
java
package com.hsh.canal02.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hsh.canal02.controller.patient.patient.PatientPageReqVO;
import com.hsh.canal02.mapper.PatientMapper;
import com.hsh.canal02.pojo.PatientDO;
import com.hsh.canal02.utils.PageResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author xrkhy
* @date 2025/11/23 16:11
* @description
*/
@Service("patientService")
@Slf4j
public class PatientServiceImpl implements PatientService{
@Resource
private PatientMapper patientMapper;
// 注入自定义的RedisTemplate
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Override
public PageResult<PatientDO> getPatientPage(PatientPageReqVO queryVO) {
// 1. 构建唯一缓存Key(包含所有分页+查询参数)
StringBuilder cacheKey = new StringBuilder("patients::");
cacheKey.append("page_").append(queryVO.getPageNo())
.append("_size_").append(queryVO.getPageSize())
.append("_name_").append(queryVO.getPatientName() == null ? "null" : queryVO.getPatientName())
.append("_phone_").append(queryVO.getPatientPhone() == null ? "null" : queryVO.getPatientPhone());
// 2. 尝试从Redis读取缓存(手动反序列化,避免类型丢失)
PageResult<PatientDO> cacheResult = null;
try {
// 手动获取缓存值,并强制转换类型(RedisTemplate已配置泛型序列化)
cacheResult = (PageResult<PatientDO>) redisTemplate.opsForValue().get(cacheKey.toString());
} catch (Exception e) {
log.warn("读取缓存失败,原因:{}", e.getMessage());
}
// 缓存命中,直接返回
if (cacheResult != null) {
return cacheResult;
}
// 3. 缓存未命中,查询数据库
IPage<PatientDO> iPage = patientMapper.selectMultipleTablePage(
new Page<>(queryVO.getPageNo(), queryVO.getPageSize()),
queryVO
);
PageResult<PatientDO> result = new PageResult<>(iPage.getRecords(), iPage.getTotal());
// 4. 写入缓存(设置30分钟过期,避免缓存雪崩)
try {
redisTemplate.opsForValue().set(cacheKey.toString(), result, 30, TimeUnit.MINUTES);
} catch (Exception e) {
log.error("写入缓存失败,原因:{}", e.getMessage());
}
return result;
}
@Override
public Integer createPatient(PatientDO patientDO) {
int insert = patientMapper.insert(patientDO);
return insert;
}
@Override
public void updatePatient(PatientDO patientDO) {
System.out.println(patientDO);
int i = patientMapper.updateById(patientDO);
if(i == 1){
redisTemplate.delete("patients::id_" + patientDO.getPatientId());
}
}
@Override
public void deletePatient(Long id) {
int i = patientMapper.deleteById(id);
if(i == 1){
redisTemplate.delete("patients::id_" + id);
}
}
@Override
public void deletePatientList(List<Long> ids) {
System.out.println(ids);
patientMapper.deleteBatchIds(ids);
}
// @Override
// public PatientDO getPatient(Long id) {
// QueryWrapper queryWrapper = new QueryWrapper();
// queryWrapper.eq("patient_id", id);
// PatientDO PatientDO = patientMapper.selectOne(queryWrapper);
// return PatientDO;
// }
/**
* 根据ID查询患者(优先查缓存,缓存缺失查库并回写缓存)
*/
@Override
public PatientDO getPatient(Long patientId) {
if (patientId == null) {
return null;
}
// String cacheKey = PATIENT_ID_PREFIX + patientId;
String cacheKey = "patients::id_" + patientId;
// 1. 先查缓存
PatientDO patient = (PatientDO) redisTemplate.opsForValue().get(cacheKey);
if (patient != null) {
return patient;
}
// 2. 缓存缺失,查数据库
patient = patientMapper.selectById(patientId);
if (patient == null) {
// 缓存空值,防止缓存穿透(设置短期过期)
redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
return null;
}
// 3. 回写缓存(设置合理过期时间,防止缓存雪崩)
redisTemplate.opsForValue().set(cacheKey, patient, 1, TimeUnit.HOURS);
return patient;
}
@Override
public List<PatientDO> getAllPatients(PatientDO patientDO) {
if (patientDO == null){
patientDO = new PatientDO();
}
// 条件查询所有所有数据
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.like(patientDO.getPatientName() != null,"patient_name", patientDO.getPatientName());
queryWrapper.like(patientDO.getPatientPhone() != null,"patient_phone", patientDO.getPatientPhone());
List<PatientDO> allPatients = patientMapper.selectList(queryWrapper);
return allPatients;
}
}
8 utils
java
package com.hsh.canal02.utils;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public final class PageResult<T> implements Serializable {
private Long total;
private List<T> list;
public PageResult() {
}
public PageResult(List<T> list, Long total) {
this.list = list;
this.total = total;
}
public PageResult(Long total) {
this.list = new ArrayList<>();
this.total = total;
}
public static <T> PageResult<T> empty() {
return new PageResult<>(0L);
}
public static <T> PageResult<T> empty(Long total) {
return new PageResult<>(total);
}
}
9 resources配置文件
mappers
PatientMapper.xml
java
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hsh.canal02.mapper.PatientMapper">
<select id="selectMultipleTablePage" resultMap="PatientWithJoinResult">
select
a.patient_id,
a.patient_name,
a.patient_type,
a.gender,
a.priority_level,
a.channel_id,
a.age,
a.doctor_id,
a.birth_date,
a.patient_phone,
a.patient_address,
a.avatar_url,
a.remarks,
a.create_time,
a.tenant_id,
b.id,
b.username,
b.nickname
from patient as a
left join system_users as b on a.doctor_id = b.id
<where>
and a.deleted = 0
<if test="query.patientId != null">
and a.patient_id = #{query.patientId}
</if>
<if test="query.patientName != null">
and a.patient_name like concat('%', #{query.patientName}, '%')
</if>
<if test="query.patientType != null">
and a.patient_type = #{query.patientType}
</if>
<if test="query.gender != null">
and a.gender = #{query.gender}
</if>
<if test="query.priorityLevel != null">
and a.priority_level = #{query.priorityLevel}
</if>
<if test="query.age != null">
and a.age = #{query.age}
</if>
<if test="query.doctorId != null">
and a.doctor_id = #{query.doctorId}
</if>
<if test="query.channelId != null">
and a.channel_id = #{query.channelId}
</if>
<if test="query.birthDate != null">
and a.birth_date between #{query.birthDate[0]} and #{query.birthDate[1]}
</if>
<if test="query.patientPhone != null">
and a.patient_phone like concat('%', #{query.patientPhone}, '%')
</if>
<if test="query.patientAddress != null">
and a.patient_address = #{query.patientAddress}
</if>
<if test="query.avatarUrl != null">
and a.avatar_url = #{query.avatarUrl}
</if>
<if test="query.remarks != null">
and a.remarks = #{query.remarks}
</if>
<if test="query.createTime != null">
and a.create_time between #{query.transformCreateTime[0]} and #{query.transformCreateTime[1]}
</if>
</where>
<!--倒序排序-->
order by a.create_time desc
</select>
<!-- 定义 resultMap -->
<resultMap id="PatientWithJoinResult" type="com.hsh.canal02.pojo.PatientDO">
<id column="patient_id" property="patientId"/>
<result column="patient_name" property="patientName"/>
<result column="patient_type" property="patientType"/>
<result column="gender" property="gender"/>
<result column="priority_level" property="priorityLevel"/>
<result column="channel_id" property="channelId"/>
<result column="age" property="age"/>
<result column="doctor_id" property="doctorId"/>
<result column="birth_date" property="birthDate"/>
<result column="patient_phone" property="patientPhone"/>
<result column="patient_address" property="patientAddress"/>
<result column="avatar_url" property="avatarUrl"/>
<result column="remarks" property="remarks"/>
<result column="create_time" property="createTime"/>
<result column="tenant_id" property="tenantId"/>
<!-- 其他字段映射 -->
<association property="user" javaType="com.hsh.canal02.pojo.AdminUserDO">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="nickname" property="nickname"/>
</association>
</resultMap>
</mapper>
10 测试
使用 接口测试工具测试比如postman,apifox
查询测试
访问localhost:8080/patient/manage/page
查看redis发现有分页的缓存,但是不会触发canal
新增测试
访问localhost:8080/patient/manage/create
查看redis发现分页的缓存被删除,但是触发canal,把新增的数据存入到了redis
修改测试
访问localhost:8080/patient/manage/update
查看redis发现分页的缓存被删除,但是触发canal,把更新的数据存入到了redis,
删除测试
访问localhost:8080/patient/manage/delete
查看redis发现分页的缓存被删除,但是触发canal,把更新的数据存入到了redis,因为这里是逻辑删除