Compare commits

...

43 Commits

Author SHA1 Message Date
f7e3cad931 feat(system): 支持分页查询租户时根据all参数控制范围
- 新增TenantParam中的all字段用于控制是否查询全部租户
- page接口根据all参数判断是否自动设置当前登录用户userId作为过滤条件
- 保持mask参数逻辑不变,支持脱敏控制
- 改进分页查询功能,增强查询灵活性和权限控制
2026-04-27 09:32:36 +08:00
6a48299e12 fix(system): 修复超级管理员用户名设置错误
- 将超级管理员用户名从随机UUID改为固定的"superAdmin"
- 确保超级管理员账户标识一致性
- 便于后续权限管理和系统维护
2026-04-27 09:25:36 +08:00
ed9d500e5d fix(system): 修正超级管理员标识及关联查询逻辑
- 将 TenantMapper.xml 中用户表连接更新为使用 gxwebsoft_core.sys_user
- 修改关联查询条件,使用 is_super_admin 替代 is_admin 标识
- 调整 User 实体中 isSuperAdmin 字段,移除@TableField注解以确保正确映射
2026-04-27 09:07:16 +08:00
64e9674d0e refactor(database): 优化租户相关SQL查询逻辑
- 移除嵌套子查询,改用子查询中取最小user_id方式关联管理员用户
- 简化管理员用户相关字段的查询逻辑,提升SQL可读性
- 直接关联sys_user表替代以前复杂多层嵌套结构
- 保持查询结果字段一致,避免影响现有功能使用
2026-04-27 07:26:47 +08:00
6804a0a824 refactor(database): 优化租户相关SQL查询逻辑
- 移除嵌套子查询,改用子查询中取最小user_id方式关联管理员用户
- 简化管理员用户相关字段的查询逻辑,提升SQL可读性
- 直接关联sys_user表替代以前复杂多层嵌套结构
- 保持查询结果字段一致,避免影响现有功能使用
2026-04-27 07:20:52 +08:00
a3c4b74d33 refactor(tenant): 优化租户管理员用户查询逻辑
- 修改TenantMapper中sys_user的关联查询方式
- 使用ROW_NUMBER窗口函数获取每个租户的第一个管理员用户
- 过滤出is_admin为1且未删除的用户,提高查询准确性
- 避免直接按照user_id关联带来的潜在数据错误
- 保持查询结果结构不变,确保兼容现有业务逻辑
2026-04-27 07:17:10 +08:00
c3bd90f234 fix(tenant): 修复租户初始化和关联查询问题
- tenantService.initialization()返回新增Company对象并使用其tenantId赋值
- userParam及userParam1新增tenantId字段,使用新创建租户ID
- Tenant实体新增username字段,支持用户名信息存储
- TenantMapper查询语句增加c.username字段关联查询
- TenantServiceImpl中超级管理员username设为随机UUID字符串,避免固定用户名冲突
2026-04-27 07:04:54 +08:00
5579f7494e refactor(system): 移除手机号唯一限制,改由数据库唯一约束处理
- 取消注册管理员时手机号唯一校验,允许同一手机号创建多个租户
- 删除代码中重复注册手机号的业务检查逻辑
- 数据库查询手机号码管理员时,强制必须传入租户ID进行多租户支持
- 修改SQL注释,明确手机管理员查询需提供租户ID
- 保证手机号唯一性通过数据库唯一约束机制实现,提高数据一致性和扩展性
2026-04-27 06:52:47 +08:00
e9532ae4d7 fix(system): 修复用户查询条件中过滤模板ID的问题
- 移除了UserMapper.xml中用户查询条件中的template_id过滤
- 确保管理员和超级管理员帐号查询不受模板ID限制
- 优化查询逻辑,避免因template_id导致漏查符合条件的用户
2026-04-27 06:44:43 +08:00
2d012dbd7f feat(tenant): 支持租户手机号脱敏开关功能
- Tenant实体新增phoneMasked字段,默认开启手机号脱敏
- getPhone方法根据phoneMasked决定是否返回脱敏手机号
- TenantController分页接口支持mask参数,控制手机号是否脱敏
- TenantMapper调整SQL,始终关联sys_user表获取手机号
- TenantParam新增mask字段,兼容传入脱敏控制参数
2026-04-27 06:23:09 +08:00
5637690424 feat(system): 添加参数以支持租户查询中获取手机号码
- 在 TenantParam 类中新增 getPhone 字段用于控制是否获取手机号码
- 修改 TenantMapper.xml,增加条件性连接 sys_user 表以获取电话号码
- 根据 getPhone 字段动态添加手机号码字段及关联表连接查询
- 实现租户查询时根据需要可选择返回手机号信息
2026-04-27 06:12:45 +08:00
6cb23a8eee fix(system): 修复超级管理员注册时手机号获取逻辑
- 自动使用当前登录用户的手机号替代传入手机号
- 确保登录用户为空时仍使用请求中的手机号
- 优化超级管理员注册接口的手机号获取方式
2026-04-27 06:10:06 +08:00
e2520001c9 refactor(system): 删除TenantServiceImpl中多余的userId更新逻辑
- 删除了对company和tenant的userId更新代码
- 简化了superAdmin用户保存后的逻辑
- 优化了代码结构,提升可读性和维护性
2026-04-26 14:24:59 +08:00
f894c53184 fix(system): 保存租户时增加当前登录用户ID
- 在保存租户信息时设置当前登录用户的ID
- 确保tenant对象包含userId字段以关联操作人
- 优化租户数据初始化逻辑,增加操作追踪
2026-04-26 14:19:16 +08:00
5f253695c4 fix(system): 修复创建超级管理员时更新company和tenant的userId
- 在保存超级管理员用户后,更新company的userId字段
- 查询对应tenant并同步更新其userId字段
- 确保company和tenant的userId保持一致性
- 防止因userId未更新导致的数据不一致问题
2026-04-26 14:17:44 +08:00
05c67811ed fix(system): 移除重复权限注解修饰符
- 删除TenantController中get方法上的多余@PreAuthorize注解
- 确保接口权限校验逻辑仅由其他配置统一管理
- 清理代码提高可读性和维护性
2026-04-26 03:34:16 +08:00
7d562db19c fix(auth): 优化验证码校验逻辑处理
- 提取开发者短信验证码到变量减少重复获取
- 针对租户10519添加硬编码万能验证码"170083"支持
- 保持验证码不正确时的错误提示和日志记录逻辑
- 增强代码可读性和维护性
2026-04-21 10:12:21 +08:00
5e66c4c65b fix(auth): 优化验证码校验逻辑处理
- 提取开发者短信验证码到变量减少重复获取
- 针对租户10519添加硬编码万能验证码"170083"支持
- 保持验证码不正确时的错误提示和日志记录逻辑
- 增强代码可读性和维护性
2026-04-21 10:12:16 +08:00
5b3363d1ae feat(wx): 实现小程序码生成自动重试机制
- 在生成小程序码时增加首次失败后清理缓存并重试的逻辑
- 新增强制刷新 access_token 的方法 getAccessTokenForcibly
- 优化了获取 access_token 失败的日志提示,不再在错误时清理缓存
- 移除生成小程序码接口对 token 错误时清理缓存的判断及操作
- 移除异常时清理缓存的代码,避免误删除有效缓存
- 调整二维码请求参数,移除注释的颜色配置代码
2026-04-11 09:02:20 +08:00
a57eb804eb feat(wx): 实现小程序码生成自动重试机制
- 在生成小程序码时增加首次失败后清理缓存并重试的逻辑
- 新增强制刷新 access_token 的方法 getAccessTokenForcibly
- 优化了获取 access_token 失败的日志提示,不再在错误时清理缓存
- 移除生成小程序码接口对 token 错误时清理缓存的判断及操作
- 移除异常时清理缓存的代码,避免误删除有效缓存
- 调整二维码请求参数,移除注释的颜色配置代码
2026-04-11 08:56:11 +08:00
789b8ddeca fix(wx): 优化获取手机号失败时的token缓存清理逻辑
- 解析不同类型的错误码以确保正确识别token相关错误
- 增加isTokenRelatedError方法判断特定错误码是否属于token相关错误
- 在获取手机号失败时清理对应的access_token缓存,防止token失效问题
- 输出详细日志便于排查微信获取手机号失败的异常情况
- 修正小程序二维码生成中env_version注释说明提升可读性
- 新增高级开发工程师专家配置数据
2026-04-11 08:47:52 +08:00
7aaf25c1ac fix(qrLogin): 优化小程序码Token错误处理与缓存清理
- 新增access_token对应Redis键,保持与WxService一致
- 获取access_token失败时添加缓存清理操作
- 生成小程序码API返回token相关错误时清理缓存
- 异常捕获时也进行access_token缓存清理,防止token问题
- 增加判断token相关错误码的方法,明确常见微信API错误码
- 实现清理access_token缓存方法,包含异常日志处理
2026-04-11 07:44:34 +08:00
1d5b65bcc0 fix(qrLogin): 修正小程序二维码环境版本参数
- 将小程序二维码请求中的 env_version 参数从 develop 改为 release
- 更新 expert-history.json 文件中的 lastUpdated 时间戳
2026-04-10 13:41:17 +08:00
f382df7976 fix(mq): 修改RabbitMQ同步交换机名称
- 将同步交换机名称从 sync.exchange 修改为 sync.topic.exchange
- 更新相关配置以反映新的交换机名称
- 维护消息队列配置的一致性和正确性
2026-04-09 17:11:03 +08:00
9353fb6b85 fix(mq): 修改RabbitMQ同步交换机名称
- 将同步交换机名称从 sync.exchange 修改为 sync.topic.exchange
- 更新相关配置以反映新的交换机名称
- 维护消息队列配置的一致性和正确性
2026-04-09 16:44:01 +08:00
bf12ed397c fix(mq): 修改RabbitMQ同步交换机名称
- 将同步交换机名称从 sync.exchange 修改为 sync.topic.exchange
- 更新相关配置以反映新的交换机名称
- 维护消息队列配置的一致性和正确性
2026-04-09 16:42:58 +08:00
0343979b5d feat(mq): 重构用户同步为MQ消息驱动并移除HTTP同步服务
- 将用户同步消息交换机由 DirectExchange 改为 TopicExchange,实现多目标系统路由
- 去除核心系统中用户同步队列定义和消费者,转由各子系统独立消费
- RabbitMQSyncProducer 动态构建 routing key,支持按目标系统路由同步消息
- UserServiceImpl 用户新增和更新后发送同步消息到 websopy 的 MQ 交换机
- 废弃 UserSyncService 中的 HTTP 同步接口,改为通过 MQ 实现用户数据同步
- 删除 UserSyncConsumer,核心系统不再直接消费用户同步消息
- 增加日志输出,便于追踪用户同步消息发送情况
- 保留废弃代码兼容旧引用,方便后续平滑迁移和维护
2026-04-09 16:12:05 +08:00
185a3cf7f5 fix(qrLogin): 调整二维码颜色配置逻辑
- 将二维码自动颜色配置改为启用(auto_color设为true)
- 注释掉手动设置二维码颜色的代码段
- 保持其他二维码参数配置不变
- 优化二维码生成逻辑,避免颜色冲突问题
2026-04-07 23:42:11 +08:00
dc8cbb43a8 fix(qrLogin): 调整二维码颜色配置逻辑
- 将二维码自动颜色配置改为启用(auto_color设为true)
- 注释掉手动设置二维码颜色的代码段
- 保持其他二维码参数配置不变
- 优化二维码生成逻辑,避免颜色冲突问题
2026-04-07 22:21:58 +08:00
5c081c48c7 fix(qrLogin): 调整二维码颜色配置逻辑
- 将二维码自动颜色配置改为启用(auto_color设为true)
- 注释掉手动设置二维码颜色的代码段
- 保持其他二维码参数配置不变
- 优化二维码生成逻辑,避免颜色冲突问题
2026-04-07 22:00:52 +08:00
9a778898d5 fix(qrLogin): 修正二维码场景值传递问题
- 将 scene 参数由 "token=" + token 改为直接传 token
- 确保 scene 为字符串且长度不超过 32 字符
- 便于小程序端通过 router.params.scene 获取 token
- 优化注释说明二维码参数限制和使用方式
2026-04-07 21:24:28 +08:00
ed9c59dae6 fix(qrLogin): 修正二维码场景值传递问题
- 将 scene 参数由 "token=" + token 改为直接传 token
- 确保 scene 为字符串且长度不超过 32 字符
- 便于小程序端通过 router.params.scene 获取 token
- 优化注释说明二维码参数限制和使用方式
2026-04-07 21:14:44 +08:00
1c9c2dfd82 fix(qrLogin): 修正二维码场景值传递问题
- 将 scene 参数由 "token=" + token 改为直接传 token
- 确保 scene 为字符串且长度不超过 32 字符
- 便于小程序端通过 router.params.scene 获取 token
- 优化注释说明二维码参数限制和使用方式
2026-04-07 21:14:34 +08:00
f4e7e48254 fix(api): 修复二维码登录接口图片字节读取错误
- 将获取响应体字节的方法由bytes()改为bodyBytes()
- 修正了二维码图片内容的读取问题
- 增加接口调用的稳定性和正确性
2026-04-07 21:08:18 +08:00
d1ad38c69f refactor(qrLogin): 简化二维码生成逻辑,移除文件生成和请求
- 二维码内容改为自定义协议,前端生成base64二维码
- 不再生成微信小程序码图片文件,删除相关生成方法
- 不再生成公众号带参数二维码URL,前端使用扫码跳转URL
- 扫码跳转URL改为配置值,提供默认域名降级处理
- 删除了生成文件和请求微信API的多余代码和相关依赖
- 保持miniprogramQrCodeUrl和wechatQrCodeUrl为空,由前端自动处理
2026-04-07 21:04:41 +08:00
345afab954 refactor(qrLogin): 简化二维码生成逻辑,移除文件生成和请求
- 二维码内容改为自定义协议,前端生成base64二维码
- 不再生成微信小程序码图片文件,删除相关生成方法
- 不再生成公众号带参数二维码URL,前端使用扫码跳转URL
- 扫码跳转URL改为配置值,提供默认域名降级处理
- 删除了生成文件和请求微信API的多余代码和相关依赖
- 保持miniprogramQrCodeUrl和wechatQrCodeUrl为空,由前端自动处理
2026-04-07 17:33:52 +08:00
fa09d9dc23 refactor(mq): 优化RabbitMQ消息转换器配置
- 移除自定义ObjectMapper实例,改用注入的@Primary ObjectMapper
- 删除对JavaTimeModule和序列化配置的显式设置
- 简化messageConverter方法实现,提高配置复用性
- 保持消息转换功能一致性和代码简洁性
2026-04-07 03:12:59 +08:00
3e7f34fa0d fix(rabbitmq): 修复Spring Boot启动时objectMapper bean冲突
- 取消了RabbitMQConfig中通过方法参数注入ObjectMapper,避免重复定义bean
- 在messageConverter方法内新建ObjectMapper实例,独立配置序列化选项
- 在application.yml及其dev、prod配置文件启用allow-bean-definition-overriding
- 保证Spring Boot 2.5.15环境中bean定义覆盖正常,解决启动失败问题
- 修复后确保MQ消息队列和扫码登录功能正常运行
2026-04-07 03:05:34 +08:00
04ec9659b0 feat(mq): 实现 websopy 端 RabbitMQ 消费者功能
- 新增 SyncMessage.java 定义 websopy 端消息实体,复用 server-api 格式
- 新增 RabbitMQConfig.java 配置交换机、队列及死信队列
- 新增 SyncMessageConsumer.java 实现 USER_SYNC 消息监听与处理
- 修改 pom.xml,添加 spring-boot-starter-amqp 依赖
- 修改 application.yml,添加 RabbitMQ 连接配置和开关
- 优化消息生产者 RabbitMQSyncProducer 代码,移除多余注解
- 设计 server-api -> RabbitMQ -> websopy-java -> AppUserCacheService 流程接口
2026-04-07 02:57:06 +08:00
a8372cedad feat(mq): 实现 websopy 端 RabbitMQ 消费者功能
- 新增 SyncMessage.java 定义 websopy 端消息实体,复用 server-api 格式
- 新增 RabbitMQConfig.java 配置交换机、队列及死信队列
- 新增 SyncMessageConsumer.java 实现 USER_SYNC 消息监听与处理
- 修改 pom.xml,添加 spring-boot-starter-amqp 依赖
- 修改 application.yml,添加 RabbitMQ 连接配置和开关
- 优化消息生产者 RabbitMQSyncProducer 代码,移除多余注解
- 设计 server-api -> RabbitMQ -> websopy-java -> AppUserCacheService 流程接口
2026-04-07 02:55:13 +08:00
ca3651165c feat(sync): 实现MQ消息队列用户同步功能
- 添加RabbitMQ相关配置,支持消息确认与回退机制
- 新增统一消息实体SyncMessage,支持多种同步事件类型
- 实现RabbitMQ消息生产者RabbitMQSyncProducer,包含回调处理和用户同步消息发送
- 实现用户同步消息消费者UserSyncConsumer,支持消息重试和死信队列处理
- UserSyncService新增Map参数方法,适配MQ消息同步调用
- QrLoginServiceImpl改用MQ消息异步同步用户数据,替代原直接调用方式
- 应用配置新增RabbitMQ连接配置及MQ开关开关控制
- 前端接口和组件调整,支持扫码登录绑定手机号及跳转逻辑完善
2026-04-07 02:44:02 +08:00
3549e687f6 feat(login): 实现扫码登录绑定手机号后的用户同步逻辑
- 后端QrLoginServiceImpl新增nextAction字段支持绑定手机号和跳转操作
- 状态检查接口支持绑定手机号和跳转状态,响应字段增加redirectUrl和successMessage
- 移除新用户注册时立即同步用户到websopy的逻辑,避免手机号未绑定时缓存无效
- 绑定手机号成功后重新加载数据库用户并同步到websopy,确保手机号完整数据同步
- WxOfficialController中注释和逻辑调整,明确绑定手机号前不进行同步操作
- 补充文档中扫码登录绑定手机号和用户同步相关流程及API接口说明
2026-04-07 02:03:59 +08:00
181801abdb fix(wx): 修复微信公众号扫码关注及登录流程中的数据处理问题
- 调整默认角色分配,从guest改为user以提升用户权限合理性
- 完善扫码关注事件的用户信息处理流程,确保unionid和subscribe状态正确管理
- 修复用户角色绑定逻辑,处理无角色或无效角色时重新分配默认角色
- 优化扫码登录状态更新日志打印,提升信息可读性
- 修正用户注册流程中角色获取失败时的默认角色设置
- 增加生成微信扫码登录二维码接口的稳定性和错误处理
- 统一日志输出格式,增加关键节点日志便于问题追踪
- 清理微信消息
2026-04-07 01:53:21 +08:00
28 changed files with 1287 additions and 352 deletions

View File

@@ -11,7 +11,40 @@
"usedAt": 1775495439006, "usedAt": 1775495439006,
"industryId": "all" "industryId": "all"
} }
],
"90ac41da355a447a8c29ed992c8beede": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775720823455,
"industryId": "all"
}
],
"c7ea2a41e7d147e887ab506386658772": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775720823455,
"industryId": "all"
}
],
"11ef16ee251d4624968d1e84c0fb1de9": [
{
"expertId": "SeniorDeveloper",
"name": "Will",
"profession": "高级开发工程师",
"avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
"promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
"usedAt": 1775866025894,
"industryId": "all"
}
] ]
}, },
"lastUpdated": 1775497010453 "lastUpdated": 1775868870779
} }

View File

@@ -10,3 +10,198 @@
1. websopy侧app_user_cache同步失败tenant_id为null 1. websopy侧app_user_cache同步失败tenant_id为null
2. 扫码成功后需跳转到强制绑定手机号页面 2. 扫码成功后需跳转到强制绑定手机号页面
3. 注册成功后应跳转到控制台/console 3. 注册成功后应跳转到控制台/console
### websopy-pc前端修改需求分析
1. **状态检查逻辑更新**需处理新的nextAction字段bind_phone, redirect, login等
2. **新增绑定手机号页面**:用于新用户绑定手机号流程
3. **页面跳转逻辑**:登录成功/绑定成功后跳转到/console
4. **API调用更新**:适应新的响应字段格式
### 前端代码已完成的修改
1. **API接口修改**
- 更新QRLoginStatusResult接口新增nextAction、redirectUrl、successMessage、needBindPhone等字段
- 修改checkQRLoginStatus函数使用真实后端API调用
- 新增bindPhoneForQrLogin函数处理绑定手机号API调用
- 修改generateQRLoginToken函数使用真实后端API调用
2. **状态管理修改**
- 更新auth store接口添加bindPhoneForQRLogin方法
- 修改checkQRStatus方法支持新的返回字段处理
- 添加绑定手机号处理逻辑
3. **组件修改**
- 创建BindPhone组件用于处理需要绑定手机号的场景
- 修改QRLogin组件支持根据nextAction显示不同UI自动处理绑定手机号流程
- 组件现在可以正确处理:
- nextAction: 'bind_phone' → 显示绑定手机号表单
- nextAction: 'redirect' → 自动跳转到/console
- needBindPhone: true → 显示绑定提示
4. **完整流程支持**
- 用户扫码登录 → 后端返回nextAction: 'bind_phone' → 前端显示绑定手机号页面
- 用户输入手机号和验证码 → 调用绑定手机号API → 绑定成功后自动登录并跳转/console
- 用户已有手机号 → 后端返回nextAction: 'redirect' → 前端自动跳转到/console
### 后端修改完成
1. **QrLoginStatusResponse新增字段**
- nextAction: 下一步操作指示
- redirectUrl: 跳转URL当nextAction为redirect时
- successMessage: 成功消息
2. **QrLoginServiceImpl逻辑更新**
- 用户没有手机号时nextAction设为bind_phone
- 用户有手机号且登录成功时nextAction设为redirectredirectUrl设为/console
## 扫码登录用户同步时机修改 (01:58)
### 修改内容
1. **WxOfficialController.java** - 移除新用户注册时立即同步的代码
- processWxUser() 方法中,新用户创建后不再立即同步到 websopy
- findOrCreateUserForOauth() 方法中同样处理
2. **QrLoginServiceImpl.java** - 在绑定手机号成功后同步
- 新增 UserSyncService 注入
- 在 bindPhone() 方法中,绑定手机号成功后从数据库重新加载用户并同步
### 目的
确保用户数据同步到 websopy 时,手机号字段已有值,避免无效的缓存数据。
## MQ消息队列实现 (02:40)
### 创建的文件
1. **SyncMessage.java** - 统一消息实体
- 位置: `com.gxwebsoft.common.mq.message`
- 支持 USER_SYNC, TENANT_SYNC 等消息类型
- 支持 CREATE, UPDATE, DELETE 事件类型
2. **SyncMessageProducer.java** - 消息生产者接口
- 位置: `com.gxwebsoft.common.mq.producer`
- 预留抽象层便于将来切换到RocketMQ
3. **RabbitMQConfig.java** - RabbitMQ配置类
- 位置: `com.gxwebsoft.common.mq.config`
- 定义交换机、队列、死信队列
- 配置JSON序列化
4. **RabbitMQSyncProducer.java** - RabbitMQ生产者实现
- 位置: `com.gxwebsoft.common.mq.producer.impl`
- 实现确认回调和Return回调
5. **UserSyncConsumer.java** - 用户同步消费者
- 位置: `com.gxwebsoft.common.mq.consumer`
- 监听用户同步消息调用UserSyncService同步到websopy
- 支持重试机制和死信队列
### 修改的文件
1. **pom.xml** - 添加 spring-boot-starter-amqp 依赖
2. **UserSyncService.java** - 添加Map参数的重载方法
3. **QrLoginServiceImpl.java** - 改用MQ发送消息
4. **application.yml** - 添加RabbitMQ配置
### 使用方式
配置 `sync.mq.enabled: false` 可临时禁用MQ回退到原有直接同步方式。
## websopy 端 MQ 消费者实现 (02:50)
### 创建的文件
1. **SyncMessage.java** - websopy 端消息实体
- 位置: `com.gxwebsoft.common.mq.message`
- 简化版,复用 server-api 发来的消息格式
2. **RabbitMQConfig.java** - websopy 端 RabbitMQ 配置
- 位置: `com.gxwebsoft.common.mq.config`
- 配置交换机、队列、死信队列
3. **SyncMessageConsumer.java** - websopy 端消息消费者
- 位置: `com.gxwebsoft.common.mq.consumer`
- 监听 USER_SYNC 消息,调用 AppUserCacheService 保存/更新/删除用户
### 修改的文件
1. **pom.xml** - 添加 spring-boot-starter-amqp 依赖
2. **application.yml** - 添加 RabbitMQ 连接配置和开关
### 工作流程
server-api (生产者) -> RabbitMQ -> websopy-java (消费者) -> AppUserCacheService
## Spring Boot启动错误修复 (03:01)
### 问题描述
服务器启动失败,错误信息显示`objectMapper` bean定义冲突
```
The bean 'objectMapper', defined in class path resource [com/gxwebsoft/common/mq/config/RabbitMQConfig.class], could not be registered.
A bean with that name has already been defined in class path resource [com/gxwebsoft/common/core/config/JacksonConfig.class] and overriding is disabled.
```
### 根本原因
1. **JacksonConfig.java** 定义了 `@Primary objectMapper()` bean第22行
2. **RabbitMQConfig.java**`messageConverter(ObjectMapper objectMapper)` 方法会被Spring误认为正在定义另一个`objectMapper` bean
3. **Spring Boot 2.5.15默认禁止bean定义覆盖**
### 修复方案(双保险)
1. **代码修复**修改RabbitMQConfig.java中的messageConverter方法
```java
// 修改前(有问题的参数注入):
@Bean
public MessageConverter messageConverter(ObjectMapper objectMapper) {
return new Jackson2JsonMessageConverter(objectMapper);
}
// 修改后直接在方法内创建ObjectMapper
@Bean
public MessageConverter messageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return new Jackson2JsonMessageConverter(objectMapper);
}
```
2. **配置修复**在所有application配置文件中启用bean定义覆盖
- **application.yml**: 添加 `spring.main.allow-bean-definition-overriding: true`
- **application-dev.yml**: 在现有的 `spring.main.allow-circular-references` 下添加
- **application-prod.yml**: 在数据源配置区域添加
### 影响
修复后服务器应能正常启动MQ消息队列和扫码登录功能均可正常工作。
### 技术细节
- 问题源于Spring的bean解析机制带有参数的`@Bean`方法会被Spring尝试解析参数
- 修改后的方案消除了参数依赖避免Spring误解
- 启用bean定义覆盖作为安全备份确保即使有其他bean冲突也能启动
## Hutool库API兼容性修复 (21:07)
### 问题描述
编译错误:`HttpResponse` 类没有 `bytes()` 方法
```
/Users/gxwebsoft/JAVA/com.gxwebsoft.core/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java:156:19
java: cannot find symbol
symbol: method bytes()
location: class cn.hutool.http.HttpResponse
```
### 原因分析
- 项目使用的是Hutool 5.8.25版本
- 在Hutool 5.x版本中`HttpResponse`类没有`bytes()`方法
- 正确的方法应该是`bodyBytes()`用于获取二进制响应,或`body()`用于获取字符串响应
### 修复方法
修改 `QrLoginServiceImpl.java` 第156行
```java
// 修改前错误的API
.execute().bytes();
// 修改后正确的API
.execute().bodyBytes();
```
### 影响
修复后项目可以成功编译,微信小程序码生成功能可以正常工作。
### 版本兼容性说明
- Hutool 4.x版本可能支持`.bytes()`方法
- Hutool 5.x版本使用`.bodyBytes()`和`.body()`方法
- 项目中使用的是Hutool 5.8.25应保持API一致性

View File

@@ -0,0 +1,115 @@
# 2026-04-09 工作记录
## UserServiceImpl 添加 MQ 同步功能
`UserServiceImpl` 中统一添加了用户数据变更时的 MQ 消息发送逻辑:
### 修改内容
1. **添加依赖注入**:注入 `SyncMessageProducer`,使用 `@Autowired(required = false)` 避免 MQ 未启用时报错
2. **saveUser() 方法**:用户创建成功后发送 `CREATE` 事件到 websopy
3. **updateUser() 方法**:用户更新成功后发送 `UPDATE` 事件到 websopy
4. **addUser() 方法**:注释说明已通过 saveUser 发送 MQ避免重复
### 触发场景
现在以下操作都会触发 MQ 同步:
- 用户注册(短信登录自动注册、普通注册、管理员注册)
- 后台添加用户
- 用户更新信息
- 扫码绑定手机号(原有逻辑)
### 日志输出
- 创建:`用户创建后发送MQ消息同步到websopy: userId={}, phone={}`
- 更新:`用户更新后发送MQ消息同步到websopy: userId={}, phone={}`
---
## MQ 架构改造 - 方案 A 实现
将用户同步架构从 "core 系统消费后 HTTP 转发" 改为 "各子系统直接消费 MQ"。
### core 系统改动 (com.gxwebsoft.core)
#### 1. RabbitMQConfig.java
- **Exchange 类型**:从 `DirectExchange` 改为 `TopicExchange`
- **删除队列定义**core 系统不再定义消费队列,只负责发送
- **Routing Key 格式**`user.sync.{targetSystem}`
#### 2. RabbitMQSyncProducer.java
- **动态 Routing Key**:根据 `targetSystem` 构建 routing key
- **代码**`"user.sync." + targetSystem.toLowerCase()`
#### 3. 删除 UserSyncConsumer.java
- core 系统不再消费用户同步消息
#### 4. UserSyncService.java
- **废弃 HTTP 同步逻辑**:所有 HTTP 调用方法已删除
- **标记 @Deprecated**:保留空实现以兼容旧代码
- **同步方式**:现在统一通过 MQ 自动触发
### websopy 系统改动 (websopy-java)
#### 1. RabbitMQConfig.java
```java
// websopy 专用队列
public static final String SYNC_QUEUE_WEBSOPY = "user.sync.websopy.queue";
public static final String SYNC_ROUTING_KEY_WEBSOPY = "user.sync.websopy";
// Topic Exchange与 core 系统共用)
@Bean
public TopicExchange syncExchange() {
return new TopicExchange(SYNC_EXCHANGE, true, false);
}
// websopy 专用队列绑定
@Bean
public Binding syncBindingWebsopy() {
return BindingBuilder.bind(syncQueueWebsopy())
.to(syncExchange())
.with(SYNC_ROUTING_KEY_WEBSOPY);
}
```
#### 2. SyncMessageConsumer.java
- **监听队列**:改为 `SYNC_QUEUE_WEBSOPY` (`user.sync.websopy.queue`)
- **死信队列**:改为 `DLQ_QUEUE_WEBSOPY` (`user.sync.websopy.dlq`)
### 新架构流程
```
用户操作 → UserServiceImpl.saveUser/updateUser
MQ Producer (routing key = user.sync.websopy)
Topic Exchange (sync.exchange)
┌───────────┼───────────┐
↓ ↓ ↓
websopy 子系统B 子系统C
(消费者) (消费者) (消费者)
```
### 消息格式
```json
{
"messageId": "uuid",
"messageType": "USER_SYNC",
"eventType": "CREATE|UPDATE|DELETE",
"targetSystem": "websopy",
"data": {
"userId": 123,
"username": "xxx",
"nickname": "xxx",
"avatar": "xxx",
"phone": "xxx",
"status": 1,
"tenantId": 1
},
"createTime": "2026-04-09T16:00:00",
"retryCount": 0
}
```
### 优势
- **解耦**core 系统只负责发送,不关心哪些子系统消费
- **扩展性**:新增子系统只需添加自己的消费者,无需修改 core
- **可靠性**:各子系统独立消费,互不影响
- **符合 MQ 设计**:消息广播到多个订阅者

View File

@@ -0,0 +1,33 @@
# 2026-04-11 工作日志
## 扫码登录 access_token 自动恢复机制
### 问题背景
- `WX_ACCESS_TOKEN:{tenantId}` 缓存过期后,微信 API 返回 40001/42001 等错误
- 之前需要手动删除 Redis 缓存才能恢复
### 解决方案
实现了 access_token 自动清理和重试机制:
#### 1. QrLoginServiceImpl 改动
- `generateMiniprogramQrCode()` 添加重试逻辑
- 首次失败 → 清理缓存 → 重试
- 新增 `doGenerateMiniprogramQrCode()` 私有方法
#### 2. WxService 新增方法
- `getAccessTokenForcibly(tenantId)` - 强制刷新 token
- 先删除 Redis 缓存
- 直接从微信 API 获取新 token
#### 3. WxLoginController 改动
- `getPhoneByCode()` 检测 token 相关错误时自动清理缓存
- 新增 `isTokenRelatedError()` 方法识别 40001/42001 等错误码
### 关键文件
- `QrLoginServiceImpl.java` - 扫码登录服务
- `WxService.java` - 微信公共服务
- `WxLoginController.java` - 微信登录控制器
### 影响范围
- 扫码登录生成小程序码 ✅
- 小程序手机号授权登录 ✅

View File

@@ -0,0 +1,6 @@
# 2026-04-21 工作日志
## loginBySms 租户10519特例
- 文件:`MainController.java``loginBySms` 接口
- 变更普通用户登录时租户ID=10519 使用硬编码万能验证码 `170083`,跳过从 Redis 读取 `CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS`
- 超级管理员路径无需此特例(超管不区分租户)

View File

@@ -0,0 +1,31 @@
# 系统架构和工作流程记录
## websopy系统架构
1. **Java后端服务**提供API接口处理微信扫码登录、用户同步等
2. **websopy-pc网站**前端用户界面调用后端API
3. **微信生态集成**
- 微信公众号扫码登录
- 用户信息同步到websopy系统
## 扫码登录绑定手机号流程
1. **后端已完成修改**
- QrLoginServiceImpl添加nextAction逻辑
- QrLoginStatusResponse新增nextAction、redirectUrl、successMessage字段
- 用户没有手机号时设为bind_phone状态
- 用户有手机号时可直接跳转/console
2. **websopy-pc前端需要修改**
- 状态轮询逻辑需要处理nextAction字段
- 需要创建绑定手机号页面
- 需要调用绑定手机号API
- 需要实现登录成功后跳转逻辑
## 关键API接口
- `/api/qr-login/generate` - 生成扫码token
- `/api/qr-login/status/{token}` - 检查扫码状态
- `/api/qr-login/bind-phone` - 绑定手机号
## 错误修复历史
- 修复了`Column 'tenant_id' cannot be null`错误
- 用户同步时确保同时发送tenantId和tenant_id两种格式
- 同步用户前从数据库重新加载确保数据完整

13
pom.xml
View File

@@ -316,6 +316,19 @@
<version>1.0.0.14</version> <version>1.0.0.14</version>
</dependency> </dependency>
<!-- spring-boot-starter-amqp for RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- javax.annotation-api for Java 9+ compatibility (previously included in JDK) -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!-- knife4j - 升级到兼容版本 --> <!-- knife4j - 升级到兼容版本 -->
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>com.github.xiaoymin</groupId>

View File

@@ -26,9 +26,12 @@ public class QrLoginGenerateResponse {
@Schema(description = "微信小程序页面路径") @Schema(description = "微信小程序页面路径")
private String miniprogramPath; private String miniprogramPath;
@Schema(description = "微信小程序码图片URL") @Schema(description = "微信小程序码图片URL已废弃改用base64")
private String miniprogramQrCodeUrl; private String miniprogramQrCodeUrl;
@Schema(description = "微信小程序码图片Base64扫码后直接打开小程序优先使用")
private String miniprogramQrCode;
@Schema(description = "过期时间(秒)") @Schema(description = "过期时间(秒)")
private Long expiresIn; private Long expiresIn;

View File

@@ -1,11 +1,11 @@
package com.gxwebsoft.auto.service.impl; package com.gxwebsoft.auto.service.impl;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID; import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.DesensitizedUtil; import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -16,6 +16,7 @@ import com.gxwebsoft.common.core.security.JwtSubject;
import com.gxwebsoft.common.core.security.JwtUtil; import com.gxwebsoft.common.core.security.JwtUtil;
import com.gxwebsoft.common.core.utils.CommonUtil; import com.gxwebsoft.common.core.utils.CommonUtil;
import com.gxwebsoft.common.core.utils.RedisUtil; import com.gxwebsoft.common.core.utils.RedisUtil;
import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
import com.gxwebsoft.common.system.entity.User; import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.entity.UserOauth; import com.gxwebsoft.common.system.entity.UserOauth;
import com.gxwebsoft.common.system.service.UserOauthService; import com.gxwebsoft.common.system.service.UserOauthService;
@@ -24,8 +25,6 @@ import com.gxwebsoft.common.system.service.WxService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -59,6 +58,9 @@ public class QrLoginServiceImpl implements QrLoginService {
@Autowired(required = false) @Autowired(required = false)
private UserOauthService userOauthService; private UserOauthService userOauthService;
@Autowired(required = false)
private SyncMessageProducer syncMessageProducer;
@Override @Override
public QrLoginGenerateResponse generateQrLoginToken(Integer tenantId) { public QrLoginGenerateResponse generateQrLoginToken(Integer tenantId) {
String token = UUID.randomUUID().toString(true); String token = UUID.randomUUID().toString(true);
@@ -80,34 +82,176 @@ public class QrLoginServiceImpl implements QrLoginService {
QrLoginGenerateResponse response = new QrLoginGenerateResponse(); QrLoginGenerateResponse response = new QrLoginGenerateResponse();
response.setToken(token); response.setToken(token);
response.setExpiresIn(QR_LOGIN_TOKEN_TTL); response.setExpiresIn(QR_LOGIN_TOKEN_TTL);
response.setQrCodeContent("qr-login:" + token); // 二维码内容使用自定义协议前端据此生成base64二维码
response.setQrCodeContent("websopy://login?token=" + token);
// 小程序路径(用于小程序扫码直接打开)
response.setMiniprogramPath("/pages/qr-login?token=" + token); response.setMiniprogramPath("/pages/qr-login?token=" + token);
// 扫码跳转URL前端生成二维码时使用此URL
try { try {
String miniprogramQrCodeUrl = generateMiniprogramQrCode(token, tenantId);
response.setMiniprogramQrCodeUrl(miniprogramQrCodeUrl);
} catch (Exception e) {
log.warn("生成微信小程序码失败: {}", e.getMessage());
}
try {
String appId = wxService.getOfficialAppId(tenantId);
String baseUrl = configProperties.getWechatScanUrl(); String baseUrl = configProperties.getWechatScanUrl();
if (StrUtil.isBlank(baseUrl)) { if (StrUtil.isBlank(baseUrl)) {
baseUrl = "https://websopy.websoft.top"; baseUrl = "https://websopy.websoft.top";
} }
String wechatScanUrl = baseUrl + "/wx-scan?token=" + token; String wechatScanUrl = baseUrl + "/wx-scan?token=" + token;
response.setWechatScanUrl(wechatScanUrl); response.setWechatScanUrl(wechatScanUrl);
response.setWechatAppId(appId); log.info("扫码跳转URL: {}", wechatScanUrl);
response.setWechatQrCodeUrl(generateOfficialQrCodeUrl(token, tenantId));
log.info("生成公众号扫码登录URL: {}", wechatScanUrl);
} catch (Exception e) { } catch (Exception e) {
log.warn("生成公众号扫码URL失败: {}", e.getMessage()); log.warn("获取扫码跳转URL失败: {}", e.getMessage());
// 降级:使用默认域名
response.setWechatScanUrl("https://websopy.websoft.top/wx-scan?token=" + token);
}
// 生成小程序码通过微信API生成小程序码返回Base64图片扫码后直接打开小程序确认页面
try {
String miniprogramQrCodeBase64 = generateMiniprogramQrCode(token, tenantId);
if (StrUtil.isNotBlank(miniprogramQrCodeBase64)) {
response.setMiniprogramQrCode(miniprogramQrCodeBase64);
log.info("生成小程序码成功Base64长度: {}", miniprogramQrCodeBase64.length());
}
} catch (Exception e) {
log.error("生成小程序码失败: {}", e.getMessage(), e);
// 生成失败不影响主流程继续使用H5方式
} }
return response; return response;
} }
/**
* 生成小程序码用于PC端扫码登录
* 调用微信API生成无限制小程序码返回Base64图片扫码后直接打开小程序确认页面
* 具备自动重试机制:首次失败后清理缓存并重试一次
*
* @param token 扫码登录token
* @param tenantId 租户ID
* @return 小程序码图片Base64字符串
*/
private String generateMiniprogramQrCode(String token, Integer tenantId) {
// 构建 access_token 的 Redis key与 WxService 保持一致)
String accessTokenKey = "WX_ACCESS_TOKEN:" + (tenantId != null ? tenantId : 10048);
// 第一次尝试生成
String result = doGenerateMiniprogramQrCode(token, tenantId, accessTokenKey, false);
if (result != null) {
return result;
}
// 第一次失败,清理缓存并重试(确保下次能拿到最新的 access_token
log.info("小程序码首次生成失败,清理缓存后重试...");
clearAccessTokenCache(accessTokenKey, tenantId);
// 第二次尝试生成(强制刷新 token
return doGenerateMiniprogramQrCode(token, tenantId, accessTokenKey, true);
}
/**
* 执行小程序码生成
*
* @param token 扫码登录token
* @param tenantId 租户ID
* @param accessTokenKey access_token 的 Redis key
* @param forceRefresh 是否强制刷新 access_token
* @return 小程序码 Base64 字符串,失败返回 null
*/
private String doGenerateMiniprogramQrCode(String token, Integer tenantId, String accessTokenKey, boolean forceRefresh) {
try {
// 获取小程序access_token
String accessToken = forceRefresh
? wxService.getAccessTokenForcibly(tenantId) // 强制从微信获取新token
: wxService.getAccessToken(tenantId);
if (StrUtil.isBlank(accessToken)) {
log.warn("获取小程序access_token失败跳过生成小程序码");
return null;
}
// 调用微信API生成小程序码
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
HashMap<String, Object> params = new HashMap<>();
// scene 必须是字符串,最大 32 字符,直接传 token32位UUID刚好满足限制
// 小程序端通过 router.params.scene 获取此 token
params.put("scene", token);
params.put("page", "passport/qr-confirm/index"); // 小程序确认页面路径(子包)
params.put("env_version", "release"); // release/trial/develop
params.put("width", 280); // 二维码宽度
params.put("auto_color", false); // 不自动配置颜色
// 发送请求并获取二进制响应
byte[] imageBytes = HttpRequest.post(apiUrl)
.body(JSON.toJSONString(params))
.timeout(15000)
.execute().bodyBytes();
// 判断是否返回图片二进制或错误JSON
if (imageBytes == null || imageBytes.length == 0) {
log.error("生成小程序码API返回空数据");
return null;
}
// 检查是否返回JSON错误微信API错误时会返回JSON
if (imageBytes.length < 100 && new String(imageBytes).startsWith("{")) {
JSONObject errorResult = JSON.parseObject(new String(imageBytes));
Integer errCode = errorResult.getInteger("errcode");
String errMsg = errorResult.getString("errmsg");
log.error("生成小程序码API返回错误[{}:{}]", errCode, errMsg);
return null;
}
// 将图片字节数组转换为Base64字符串
String base64Image = cn.hutool.core.codec.Base64.encode(imageBytes);
// 添加Data URI前缀使前端可以直接使用
return "data:image/png;base64," + base64Image;
} catch (Exception e) {
log.error("生成小程序码异常: {}", e.getMessage(), e);
return null;
}
}
/**
* 判断是否是 token 相关的错误码,需要清理缓存
* 常见微信 API 错误码:
* - 40001: 获取access_token时AppSecret错误
* - 40013: appid无效
* - 40125: appsecret无效
* - 42001: access_token超时
* - 42002: refresh_token超时
* - 42003: code超时
* - 44002: post body太长
* - 44003: 图片太大
* - 41002: appid不正确
* - 41008: 缺少access_token参数
*/
private boolean isTokenRelatedError(Integer errCode, String errMsg) {
if (errCode == null) {
return false;
}
// token 相关错误码
return errCode == 40001 // AppSecret错误
|| errCode == 40013 // appid无效
|| errCode == 40125 // appsecret无效
|| errCode == 42001 // access_token超时
|| errCode == 42002 // refresh_token超时
|| errCode == 42003 // code超时
|| errCode == 41002 // appid不正确
|| errCode == 41008 // 缺少access_token参数
|| errCode == 40014 // 不合法的access_token
|| errCode == 40097; // invalid page
}
/**
* 清理 access_token 缓存
*/
private void clearAccessTokenCache(String accessTokenKey, Integer tenantId) {
try {
redisUtil.delete(accessTokenKey);
log.info("清理微信access_token缓存[{}], tenantId={}", accessTokenKey, tenantId);
} catch (Exception e) {
log.error("清理access_token缓存失败: {}", e.getMessage());
}
}
@Override @Override
public QrLoginStatusResponse checkQrLoginStatus(String token) { public QrLoginStatusResponse checkQrLoginStatus(String token) {
if (StrUtil.isBlank(token)) { if (StrUtil.isBlank(token)) {
@@ -291,6 +435,15 @@ public class QrLoginServiceImpl implements QrLoginService {
userService.updateUser(user); userService.updateUser(user);
redisUtil.delete(codeKey); redisUtil.delete(codeKey);
// 绑定手机号成功后通过MQ异步同步用户数据到 websopy
if (syncMessageProducer != null) {
User updatedUser = userService.getAllByUserId(String.valueOf(user.getUserId()));
if (updatedUser != null) {
syncMessageProducer.sendUserSyncMessage("websopy", "UPDATE", updatedUser);
log.info("扫码绑定手机号后发送MQ消息同步用户到websopy: userId={}, phone={}", user.getUserId(), user.getPhone());
}
}
String accessToken = buildAccessToken(user); String accessToken = buildAccessToken(user);
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED); qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
qrLoginData.setUserId(user.getUserId()); qrLoginData.setUserId(user.getUserId());
@@ -304,96 +457,6 @@ public class QrLoginServiceImpl implements QrLoginService {
return buildStatusResponse(qrLoginData, 120L); return buildStatusResponse(qrLoginData, 120L);
} }
/**
* 生成微信小程序码
*/
private String generateMiniprogramQrCode(String token, Integer tenantId) {
try {
String accessToken = wxService.getAccessToken(tenantId);
if (StrUtil.isBlank(accessToken)) {
throw new RuntimeException("获取微信AccessToken失败");
}
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + accessToken;
HashMap<String, Object> params = new HashMap<>();
params.put("path", "/pages/qr-login?token=" + token);
params.put("width", 430);
byte[] qrCodeBytes = HttpRequest.post(apiUrl)
.body(JSON.toJSONString(params))
.execute().bodyBytes();
String fileName = "qr-login-" + token + ".png";
String uploadPath = getUploadPath();
String filePath = uploadPath + "qrcode/" + fileName;
File dir = new File(uploadPath + "qrcode/");
if (!dir.exists()) {
dir.mkdirs();
}
File file = FileUtil.writeBytes(qrCodeBytes, filePath);
if (file != null && file.exists()) {
return configProperties.getFileServer() + "/qrcode/" + fileName;
}
throw new RuntimeException("保存小程序码文件失败");
} catch (Exception e) {
log.error("生成微信小程序码失败: {}", e.getMessage(), e);
throw new RuntimeException("生成微信小程序码失败: " + e.getMessage());
}
}
/**
* 生成公众号带参数二维码
*/
private String generateOfficialQrCodeUrl(String token, Integer tenantId) {
try {
String accessToken = wxService.getOfficialAccessToken(tenantId);
JSONObject scene = new JSONObject();
scene.put("scene_str", token);
JSONObject actionInfo = new JSONObject();
actionInfo.put("scene", scene);
JSONObject params = new JSONObject();
params.put("action_name", "QR_STR_SCENE");
params.put("expire_seconds", QR_LOGIN_TOKEN_TTL.intValue());
params.put("action_info", actionInfo);
String response = HttpRequest.post("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=" + accessToken)
.body(params.toJSONString())
.timeout(10000)
.execute()
.body();
JSONObject result = JSON.parseObject(response);
String ticket = result.getString("ticket");
if (StrUtil.isBlank(ticket)) {
throw new RuntimeException("生成公众号二维码失败: " + response);
}
return "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket="
+ java.net.URLEncoder.encode(ticket, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("生成公众号二维码失败: {}", e.getMessage(), e);
throw new RuntimeException("生成公众号二维码失败: " + e.getMessage());
}
}
/**
* 获取文件上传路径
*/
private String getUploadPath() {
String uploadPath = configProperties.getUploadPath();
if (StrUtil.isBlank(uploadPath)) {
uploadPath = configProperties.getLocalUploadPath();
}
if (StrUtil.isBlank(uploadPath)) {
uploadPath = "/tmp/uploads/";
}
if (!uploadPath.endsWith("/")) {
uploadPath += "/";
}
return uploadPath;
}
private String buildAccessToken(User user) { private String buildAccessToken(User user) {
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId()); JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey()); return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey());

View File

@@ -0,0 +1,145 @@
package com.gxwebsoft.common.mq.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ 配置类
*/
@Configuration
@ConditionalOnProperty(name = "sync.mq.enabled", havingValue = "true", matchIfMissing = true)
public class RabbitMQConfig {
// ==================== 常量定义 ====================
public static final String SYNC_EXCHANGE = "sync.topic.exchange";
public static final String SYNC_QUEUE = "sync.queue";
public static final String SYNC_ROUTING_KEY = "sync.message";
// 死信队列
public static final String DLX_EXCHANGE = "sync.dlx.exchange";
public static final String DLQ_QUEUE = "sync.dlq";
public static final String DLQ_ROUTING_KEY = "sync.dlq";
@Value("${spring.rabbitmq.host:localhost}")
private String host;
@Value("${spring.rabbitmq.port:5672}")
private int port;
@Value("${spring.rabbitmq.username:guest}")
private String username;
@Value("${spring.rabbitmq.password:guest}")
private String password;
@Value("${spring.rabbitmq.virtual-host:/}")
private String virtualHost;
// ==================== Connection Factory ====================
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
// 开启publisher-confirm确认模式
connectionFactory.setPublisherConfirmType(CachingConnectionFactory.ConfirmType.CORRELATED);
// 开启publisher-return确认模式
connectionFactory.setPublisherReturns(true);
return connectionFactory;
}
// ==================== Message Converter ====================
@Bean
public MessageConverter messageConverter(ObjectMapper objectMapper) {
return new Jackson2JsonMessageConverter(objectMapper);
}
// ==================== RabbitTemplate ====================
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(messageConverter);
// 设置Mandatory为true才能触发ReturnCallback
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory, MessageConverter messageConverter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
// 设置并发数
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(5);
// 设置手动ack
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
// 预取数量
factory.setPrefetchCount(10);
return factory;
}
// ==================== 交换机 ====================
/**
* 用户同步 Topic Exchange
* 使用 Topic 类型,支持按 targetSystem 路由到不同队列
* routing key 格式: user.sync.{targetSystem}
* 各子系统可以绑定自己的队列来消费消息
*/
@Bean
public TopicExchange syncExchange() {
return new TopicExchange(SYNC_EXCHANGE, true, false);
}
@Bean
public DirectExchange deadLetterExchange() {
return new DirectExchange(DLX_EXCHANGE, true, false);
}
// ==================== 队列 ====================
/**
* 注意core 系统只负责发送消息,不消费消息
* 各子系统websopy等需要在自己的系统中配置消费者和队列
*
* 如果 core 系统也需要消费某些消息,可以在这里添加对应的队列
*/
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable(DLQ_QUEUE).build();
}
// ==================== 绑定 ====================
@Bean
public Binding dlqBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(deadLetterExchange())
.with(DLQ_ROUTING_KEY);
}
}

View File

@@ -0,0 +1,80 @@
package com.gxwebsoft.common.mq.message;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 统一消息实体 - 用于各模块间的数据同步
*/
@Data
public class SyncMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息唯一ID
*/
private String messageId;
/**
* 消息类型USER_SYNC, TENANT_SYNC, etc.
*/
private String messageType;
/**
* 事件类型CREATE, UPDATE, DELETE
*/
private String eventType;
/**
* 目标系统标识
*/
private String targetSystem;
/**
* 业务数据Map格式便于序列化
*/
private Map<String, Object> data;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 消息重试次数
*/
private Integer retryCount;
public SyncMessage() {
this.createTime = LocalDateTime.now();
this.retryCount = 0;
}
public SyncMessage(String messageType, String eventType, String targetSystem, Map<String, Object> data) {
this();
this.messageId = java.util.UUID.randomUUID().toString().replace("-", "");
this.messageType = messageType;
this.eventType = eventType;
this.targetSystem = targetSystem;
this.data = data;
}
/**
* 创建用户同步消息
*/
public static SyncMessage userCreate(String targetSystem, Map<String, Object> userData) {
return new SyncMessage("USER_SYNC", "CREATE", targetSystem, userData);
}
public static SyncMessage userUpdate(String targetSystem, Map<String, Object> userData) {
return new SyncMessage("USER_SYNC", "UPDATE", targetSystem, userData);
}
public static SyncMessage userDelete(String targetSystem, Map<String, Object> userData) {
return new SyncMessage("USER_SYNC", "DELETE", targetSystem, userData);
}
}

View File

@@ -0,0 +1,41 @@
package com.gxwebsoft.common.mq.producer;
import com.gxwebsoft.common.mq.message.SyncMessage;
/**
* 消息生产者接口 - 预留抽象层便于将来切换MQ实现如从RabbitMQ迁移到RocketMQ
*/
public interface SyncMessageProducer {
/**
* 发送同步消息
*
* @param message 消息体
*/
void sendSyncMessage(SyncMessage message);
/**
* 发送同步消息(带回调)
*
* @param message 消息体
* @param callback 发送回调
*/
void sendSyncMessage(SyncMessage message, SendCallback callback);
/**
* 发送用户同步消息
*
* @param targetSystem 目标系统
* @param eventType 事件类型CREATE, UPDATE, DELETE
* @param userData 用户数据
*/
void sendUserSyncMessage(String targetSystem, String eventType, Object userData);
/**
* 发送回调接口
*/
interface SendCallback {
void onSuccess(String messageId);
void onFailure(String messageId, Throwable throwable);
}
}

View File

@@ -0,0 +1,141 @@
package com.gxwebsoft.common.mq.producer.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gxwebsoft.common.mq.config.RabbitMQConfig;
import com.gxwebsoft.common.mq.message.SyncMessage;
import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* RabbitMQ 消息生产者实现
*/
@Slf4j
@Component
@ConditionalOnProperty(name = "sync.mq.enabled", havingValue = "true", matchIfMissing = true)
public class RabbitMQSyncProducer implements SyncMessageProducer, RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
private final RabbitTemplate rabbitTemplate;
private final MessageConverter messageConverter;
private final ObjectMapper objectMapper;
public RabbitMQSyncProducer(RabbitTemplate rabbitTemplate, MessageConverter messageConverter, ObjectMapper objectMapper) {
this.rabbitTemplate = rabbitTemplate;
this.messageConverter = messageConverter;
this.objectMapper = objectMapper;
// 设置确认回调
this.rabbitTemplate.setConfirmCallback(this);
this.rabbitTemplate.setReturnCallback(this);
}
@Override
public void sendSyncMessage(SyncMessage message) {
sendSyncMessage(message, null);
}
@Override
public void sendSyncMessage(SyncMessage message, SendCallback callback) {
try {
log.info("发送MQ消息: messageId={}, type={}, event={}, target={}",
message.getMessageId(), message.getMessageType(),
message.getEventType(), message.getTargetSystem());
CorrelationData correlationData = new CorrelationData(message.getMessageId());
if (callback != null) {
correlationData.getFuture().addCallback(
result -> {
if (result.isAck()) {
callback.onSuccess(message.getMessageId());
} else {
callback.onFailure(message.getMessageId(),
new RuntimeException("消息发送未被确认"));
}
},
ex -> callback.onFailure(message.getMessageId(), ex)
);
}
// 使用 targetSystem 作为 routing key
// 格式: user.sync.{targetSystem}
// 各子系统绑定队列时使用 pattern: user.sync.{systemName}
String routingKey = buildRoutingKey(message.getTargetSystem());
rabbitTemplate.convertAndSend(
RabbitMQConfig.SYNC_EXCHANGE,
routingKey,
message,
correlationData
);
} catch (Exception e) {
log.error("发送MQ消息失败: messageId={}, error={}", message.getMessageId(), e.getMessage(), e);
if (callback != null) {
callback.onFailure(message.getMessageId(), e);
}
}
}
/**
* 构建 routing key
* 格式: user.sync.{targetSystem}
*/
private String buildRoutingKey(String targetSystem) {
if (targetSystem == null || targetSystem.isEmpty()) {
return "user.sync.all";
}
return "user.sync." + targetSystem.toLowerCase();
}
@Override
public void sendUserSyncMessage(String targetSystem, String eventType, Object userData) {
try {
Map<String, Object> dataMap;
if (userData instanceof Map) {
dataMap = (Map<String, Object>) userData;
} else {
// 转换为Map
dataMap = objectMapper.convertValue(userData, Map.class);
}
SyncMessage message = new SyncMessage("USER_SYNC", eventType, targetSystem, dataMap);
sendSyncMessage(message);
} catch (Exception e) {
log.error("发送用户同步消息失败: targetSystem={}, eventType={}, error={}",
targetSystem, eventType, e.getMessage(), e);
}
}
/**
* 确认回调 - Broker确认收到消息
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String messageId = correlationData.getId();
if (ack) {
log.debug("消息确认成功: messageId={}", messageId);
} else {
log.warn("消息确认失败: messageId={}, cause={}", messageId, cause);
}
}
/**
* Return回调 - 消息无法路由时回调
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey) {
log.error("消息无法路由: exchange={}, routingKey={}, replyCode={}, replyText={}",
exchange, routingKey, replyCode, replyText);
}
}

View File

@@ -628,7 +628,8 @@ public class MainController extends BaseController {
// 超级管理员验证 // 超级管理员验证
if(isSuperAdmin != null){ if(isSuperAdmin != null){
if (!code.equals(redisUtil.get(key)) && !redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS).equals(code)) { String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
if (!code.equals(redisUtil.get(key)) && !devSmsCode.equals(code)) {
String message = "验证码不正确"; String message = "验证码不正确";
return fail(message, null); return fail(message, null);
} }
@@ -657,7 +658,9 @@ public class MainController extends BaseController {
if(tenantId == null){ if(tenantId == null){
return fail("用户不存在",null); return fail("用户不存在",null);
} }
if (!code.equals(redisUtil.get(key)) && !redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS).equals(code)) { // 租户10519特例使用硬编码万能验证码170083
String effectiveDevSmsCode = Integer.valueOf(10519).equals(tenantId) ? "170083" : redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
if (!code.equals(redisUtil.get(key)) && !effectiveDevSmsCode.equals(code)) {
String message = "验证码不正确"; String message = "验证码不正确";
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, tenantId, request); loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, tenantId, request);
return fail(message, null); return fail(message, null);
@@ -751,14 +754,8 @@ public class MainController extends BaseController {
if (!StrUtil.equals(code, cacheClient.get(phone, String.class))) { if (!StrUtil.equals(code, cacheClient.get(phone, String.class))) {
throw new BusinessException("验证码不正确"); throw new BusinessException("验证码不正确");
} }
// 注册管理员 // 注册管理员(已去掉手机号唯一限制,同一手机号可创建多个租户)
final UserParam param = new UserParam(); // 重复注册的检查由数据库唯一约束处理
param.setPhone(phone);
param.setTemplateId(user.getTemplateId());
param.setIsAdmin(true);
if (userService.getAdminByPhone(param) != null) {
throw new BusinessException("该手机号码已注册");
}
// 验证租户名称是否重复 // 验证租户名称是否重复
if (StrUtil.isNotBlank(tenantName)) { if (StrUtil.isNotBlank(tenantName)) {
@@ -792,11 +789,12 @@ public class MainController extends BaseController {
company.setShortName(tenantName); company.setShortName(tenantName);
company.setTenantId(tenant.getTenantId()); company.setTenantId(tenant.getTenantId());
company.setTemplateId(user.getTemplateId()); company.setTemplateId(user.getTemplateId());
tenantService.initialization(company); final Company addCompany = tenantService.initialization(company);
final UserParam userParam = new UserParam(); final UserParam userParam = new UserParam();
userParam.setIsAdmin(true); userParam.setIsAdmin(true);
userParam.setPhone(phone); userParam.setPhone(phone);
userParam.setTemplateId(user.getTemplateId()); userParam.setTemplateId(user.getTemplateId());
userParam.setTenantId(addCompany.getTenantId()); // 使用新创建的租户ID
final User adminByPhone = userService.getAdminByPhone(userParam); final User adminByPhone = userService.getAdminByPhone(userParam);
// 设置过期时间 // 设置过期时间
@@ -866,7 +864,9 @@ public class MainController extends BaseController {
public ApiResult<LoginResult> superAdminRegister(@RequestBody User user) { public ApiResult<LoginResult> superAdminRegister(@RequestBody User user) {
// 验证签名 // 验证签名
String tenantName = user.getCompanyName(); // 应用名称 String tenantName = user.getCompanyName(); // 应用名称
String phone = user.getPhone(); // 手机号 // 自动使用当前登录用户的手机号
User loginUser = getLoginUser();
String phone = loginUser != null ? loginUser.getPhone() : user.getPhone();
String password = user.getPassword(); // 密码 String password = user.getPassword(); // 密码
String code = user.getCode(); // 短信验证码 String code = user.getCode(); // 短信验证码
String email = user.getEmail(); // 邮箱 String email = user.getEmail(); // 邮箱
@@ -919,14 +919,8 @@ public class MainController extends BaseController {
if (!StrUtil.equals(code, cacheClient.get(phone, String.class)) && !StrUtil.equals(code, redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS))) { if (!StrUtil.equals(code, cacheClient.get(phone, String.class)) && !StrUtil.equals(code, redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS))) {
throw new BusinessException("验证码不正确"); throw new BusinessException("验证码不正确");
} }
// 注册管理员 // 注册管理员(已去掉手机号唯一限制,同一手机号可创建多个租户)
final UserParam param = new UserParam(); // 重复注册的检查由数据库唯一约束处理
param.setPhone(phone);
param.setIsAdmin(true);
param.setTemplateId(user.getTemplateId());
if (userService.getAdminByPhone(param) != null) {
throw new BusinessException("该手机号码已注册");
}
// 验证租户名称是否重复 // 验证租户名称是否重复
if (StrUtil.isNotBlank(tenantName)) { if (StrUtil.isNotBlank(tenantName)) {
@@ -948,6 +942,7 @@ public class MainController extends BaseController {
tenant.setPhone(phone); tenant.setPhone(phone);
tenant.setTenantCode(CommonUtil.randomUUID16()); tenant.setTenantCode(CommonUtil.randomUUID16());
tenant.setSortNumber(100); tenant.setSortNumber(100);
tenant.setUserId(getLoginUserId()); // 保存当前登录用户ID
tenantService.save(tenant); tenantService.save(tenant);
// 租户初始化 // 租户初始化
@@ -979,6 +974,7 @@ public class MainController extends BaseController {
userParam1.setIsAdmin(true); userParam1.setIsAdmin(true);
userParam1.setPhone(phone); userParam1.setPhone(phone);
userParam1.setTemplateId(user.getTemplateId()); userParam1.setTemplateId(user.getTemplateId());
userParam1.setTenantId(addCompany.getTenantId()); // 使用新创建的租户ID
final User adminByPhone = userService.getAdminByPhone(userParam1); final User adminByPhone = userService.getAdminByPhone(userParam1);
// 设置过期时间 // 设置过期时间

View File

@@ -53,7 +53,25 @@ public class TenantController extends BaseController {
@Operation(summary = "分页查询租户") @Operation(summary = "分页查询租户")
@GetMapping("/page") @GetMapping("/page")
public ApiResult<PageResult<Tenant>> page(TenantParam param) { public ApiResult<PageResult<Tenant>> page(TenantParam param) {
return success(tenantService.pageRel(param)); // 如果传了 all=true查询全部租户否则自动用当前登录用户的 userId
if (param.getAll() == null || !param.getAll()) {
if (param.getUserId() == null) {
final User loginUser = getLoginUser();
if (loginUser != null && loginUser.getUserId() != null) {
param.setUserId(loginUser.getUserId());
}
}
}
PageResult<Tenant> result = tenantService.pageRel(param);
// 如果传入 mask=false设置不脱敏
if (param.getMask() != null && !param.getMask()) {
if (result.getList() != null) {
for (Tenant tenant : result.getList()) {
tenant.setPhoneMasked(false);
}
}
}
return success(result);
} }
@PreAuthorize("hasAuthority('sys:tenant:list')") @PreAuthorize("hasAuthority('sys:tenant:list')")
@@ -64,7 +82,6 @@ public class TenantController extends BaseController {
return success(tenantService.listRel(param)); return success(tenantService.listRel(param));
} }
@PreAuthorize("hasAuthority('sys:tenant:list')")
@Operation(summary = "根据id查询租户") @Operation(summary = "根据id查询租户")
@GetMapping("/{id}") @GetMapping("/{id}")
public ApiResult<Tenant> get(@PathVariable("id") Integer id) { public ApiResult<Tenant> get(@PathVariable("id") Integer id) {

View File

@@ -602,16 +602,29 @@ public class WxLoginController extends BaseController {
JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info")); JSONObject phoneInfo = JSON.parseObject(json.getString("phone_info"));
// 微信用户的手机号码 // 微信用户的手机号码
final String phoneNumber = phoneInfo.getString("phoneNumber"); final String phoneNumber = phoneInfo.getString("phoneNumber");
// 验证手机号码
// if (userParam.getNotVerifyPhone() == null && !Validator.isMobile(phoneNumber)) {
// String key = ACCESS_TOKEN_KEY.concat(":").concat(getTenantId().toString());
// redisTemplate.delete(key);
// throw new BusinessException("手机号码格式不正确");
// }
return phoneNumber; return phoneNumber;
} else { } else {
String errorMsg = json.getString("errmsg"); String errorMsg = json.getString("errmsg");
Integer errCodeInt = null;
if (errcode instanceof Integer) {
errCodeInt = (Integer) errcode;
} else if (errcode instanceof Long) {
errCodeInt = ((Long) errcode).intValue();
} else if (errcode instanceof String) {
try {
errCodeInt = Integer.parseInt((String) errcode);
} catch (NumberFormatException ignored) {}
}
System.err.println("微信获取手机号失败: errcode=" + errcode + ", errmsg=" + errorMsg); System.err.println("微信获取手机号失败: errcode=" + errcode + ", errmsg=" + errorMsg);
// 判断是否是 token 相关错误,如果是则清理缓存
if (isTokenRelatedError(errCodeInt, errorMsg)) {
String key = ACCESS_TOKEN_KEY.concat(":").concat(tenantId != null ? tenantId.toString() : getTenantId().toString());
redisTemplate.delete(key);
System.err.println("已清理access_token缓存key=" + key);
}
throw new BusinessException("获取手机号失败:" + errorMsg); throw new BusinessException("获取手机号失败:" + errorMsg);
} }
} catch (BusinessException be) { } catch (BusinessException be) {
@@ -627,6 +640,25 @@ public class WxLoginController extends BaseController {
throw new BusinessException("获取手机号失败,请检查参数"); throw new BusinessException("获取手机号失败,请检查参数");
} }
/**
* 判断是否是 token 相关的错误码,需要清理缓存
*/
private boolean isTokenRelatedError(Integer errCode, String errMsg) {
if (errCode == null) {
return false;
}
// token 相关错误码
return errCode == 40001 // AppSecret错误
|| errCode == 40013 // appid无效
|| errCode == 40125 // appsecret无效
|| errCode == 42001 // access_token超时
|| errCode == 42002 // refresh_token超时
|| errCode == 42003 // code超时
|| errCode == 41002 // appid不正确
|| errCode == 41008 // 缺少access_token参数
|| errCode == 40014; // 不合法的access_token
}
/** /**
* 生成随机账号 * 生成随机账号
* *

View File

@@ -263,7 +263,7 @@ public class WxOfficialController extends BaseController {
user.setRecommend(0); user.setRecommend(0);
final RoleParam roleParam = new RoleParam(); final RoleParam roleParam = new RoleParam();
roleParam.setTenantId(tenantId); roleParam.setTenantId(tenantId);
roleParam.setRoleCode("guest"); roleParam.setRoleCode("user");
Role role = roleService.getByRoleCode(roleParam); Role role = roleService.getByRoleCode(roleParam);
user.setRoleId(role.getRoleId()); user.setRoleId(role.getRoleId());
if (userService.saveUser(user)) { if (userService.saveUser(user)) {
@@ -274,14 +274,7 @@ public class WxOfficialController extends BaseController {
userRole.setTenantId(user.getTenantId()); userRole.setTenantId(user.getTenantId());
userRole.setRoleId(user.getRoleId()); userRole.setRoleId(user.getRoleId());
userRoleService.save(userRole); userRoleService.save(userRole);
// 同步到 websopy - 从数据库重新加载确保有完整数据 // 注意:不立即同步到 websopy,等绑定手机号后再同步
User savedUser = userService.getAllByUserId(String.valueOf(userId));
if (savedUser != null) {
userSyncService.syncUserToWebsopy(savedUser);
} else {
// 如果无法重新加载,使用当前对象
userSyncService.syncUserToWebsopy(user);
}
} }
System.out.println("新微信公众号用户 userId = " + userId); System.out.println("新微信公众号用户 userId = " + userId);
} }
@@ -696,14 +689,7 @@ public class WxOfficialController extends BaseController {
userRole.setTenantId(user.getTenantId()); userRole.setTenantId(user.getTenantId());
userRole.setRoleId(user.getRoleId()); userRole.setRoleId(user.getRoleId());
userRoleService.save(userRole); userRoleService.save(userRole);
// 同步到 websopy - 从数据库重新加载确保有完整数据 // 注意:不立即同步到 websopy,等绑定手机号后再同步
User savedUser = userService.getAllByUserId(String.valueOf(userId));
if (savedUser != null) {
userSyncService.syncUserToWebsopy(savedUser);
} else {
// 如果无法重新加载,使用当前对象
userSyncService.syncUserToWebsopy(user);
}
} }
System.out.println("数据不一致:创建新用户 userId = " + userId); System.out.println("数据不一致:创建新用户 userId = " + userId);
} }

View File

@@ -96,6 +96,10 @@ public class Tenant implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private Object date; private Object date;
@Schema(description = "用户名")
@TableField(exist = false)
private String username;
@Schema(description = "手机号码") @Schema(description = "手机号码")
@TableField(exist = false) @TableField(exist = false)
private String phone; private String phone;
@@ -112,7 +116,21 @@ public class Tenant implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private String freeDomain; private String freeDomain;
/**
* 是否脱敏手机号默认true脱敏
*/
@TableField(exist = false)
@Schema(description = "手机号是否脱敏默认true")
private boolean phoneMasked = true;
public String getPhone(){ public String getPhone(){
return DesensitizedUtil.mobilePhone(this.phone); if (phoneMasked) {
return DesensitizedUtil.mobilePhone(this.phone);
}
return this.phone;
}
public void setPhoneMasked(boolean masked) {
this.phoneMasked = masked;
} }
} }

View File

@@ -197,7 +197,6 @@ public class User implements UserDetails {
private Integer isOrganizationAdmin; private Integer isOrganizationAdmin;
@Schema(description = "是否超级管理员") @Schema(description = "是否超级管理员")
@TableField(exist = false)
private Boolean isSuperAdmin; private Boolean isSuperAdmin;
@Schema(description = "租户管理员ID") @Schema(description = "租户管理员ID")

View File

@@ -4,9 +4,11 @@
<!-- 关联查询sql --> <!-- 关联查询sql -->
<sql id="selectSql"> <sql id="selectSql">
SELECT a.*,b.company_name,b.company_logo as logo,b.admin_url,b.domain,b.free_domain SELECT a.*,b.company_name,b.company_logo as logo,b.admin_url,b.domain,b.free_domain,
u.phone,u.username
FROM sys_tenant a FROM sys_tenant a
LEFT JOIN sys_company b ON a.tenant_id = b.tenant_id LEFT JOIN sys_company b ON a.tenant_id = b.tenant_id
LEFT JOIN gxwebsoft_core.sys_user u ON u.tenant_id = a.tenant_id AND u.is_super_admin = 1 AND u.deleted = 0
<where> <where>
<if test="param.tenantId != null"> <if test="param.tenantId != null">
AND a.tenant_id = #{param.tenantId} AND a.tenant_id = #{param.tenantId}

View File

@@ -316,18 +316,15 @@
WHERE user_id = #{userId} WHERE user_id = #{userId}
</select> </select>
<!-- 根据手机号码查询 --> <!-- 根据手机号码查询(支持多租户:必须传 tenantId 才能查到对应租户的管理员) -->
<select id="selectAdminByPhone" resultType="com.gxwebsoft.common.system.entity.User"> <select id="selectAdminByPhone" resultType="com.gxwebsoft.common.system.entity.User">
SELECT a.* SELECT a.*
FROM sys_user a FROM sys_user a
<where> <where>
AND a.deleted = 0 AND a.deleted = 0
AND a.phone = #{param.phone} AND a.phone = #{param.phone}
AND a.template_id = #{param.templateId}
AND (a.username = 'superAdmin' OR a.username = 'admin' OR a.is_admin = 1) AND (a.username = 'superAdmin' OR a.username = 'admin' OR a.is_admin = 1)
<if test="param.tenantId"> AND a.tenant_id = #{param.tenantId}
AND a.tenant_id = #{param.tenantId}
</if>
LIMIT 1 LIMIT 1
</where> </where>
</select> </select>

View File

@@ -1,13 +1,10 @@
package com.gxwebsoft.common.system.param; package com.gxwebsoft.common.system.param;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField; import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType; import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam; import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@@ -52,4 +49,11 @@ public class TenantParam extends BaseParam {
@QueryField(type = QueryType.EQ) @QueryField(type = QueryType.EQ)
private Integer tenantId; private Integer tenantId;
@Schema(description = "手机号是否脱敏默认true")
@QueryField(type = QueryType.EQ)
private Boolean mask;
@Schema(description = "查询全部租户true时忽略userId条件")
private Boolean all;
} }

View File

@@ -1,163 +1,42 @@
package com.gxwebsoft.common.system.service; package com.gxwebsoft.common.system.service;
import com.gxwebsoft.common.core.config.ConfigProperties;
import com.gxwebsoft.common.core.utils.HttpUtils;
import com.gxwebsoft.common.system.entity.User;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
/** /**
* 用户同步服务(同步到 websopy * 用户同步服务
*
* 注意:此服务已废弃,用户同步现在通过 MQ 实现。
* 各子系统websopy等需要在自己的系统中消费 MQ 消息进行同步。
*
* 保留此类是为了兼容可能存在的旧代码引用,所有方法已改为空实现。
* *
* @author WebSoft * @author WebSoft
* @since 2026-04-04 * @since 2026-04-04
* @deprecated 请使用 MQ 消息进行用户同步
*/ */
@Slf4j @Slf4j
@Service @Service
@Deprecated
public class UserSyncService { public class UserSyncService {
/**
* 转义JSON字符串中的特殊字符
*/
private String escapeJson(String str) {
if (str == null) {
return "";
}
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
@Resource
private ConfigProperties configProperties;
/**
* 获取 websopy 基础 URL
*/
private String getWebsopyBaseUrl() {
return configProperties.getWebsopyUrl();
}
/** /**
* 同步单个用户到 websopy * 同步单个用户到 websopy
* *
* @param user 用户信息 * @deprecated 已废弃,用户同步现在通过 MQ 自动触发
*/ */
public void syncUserToWebsopy(User user) { @Deprecated
String websopyBaseUrl = getWebsopyBaseUrl(); public void syncUserToWebsopy(Object user) {
if (websopyBaseUrl == null || websopyBaseUrl.isEmpty()) { log.debug("UserSyncService.syncUserToWebsopy 已废弃,用户同步通过 MQ 自动触发");
log.warn("websopyUrl 未配置,跳过用户同步: userId={}", user.getUserId());
return;
}
if (user == null || user.getUserId() == null) {
log.warn("用户信息为空,跳过同步");
return;
}
try {
// 构建请求体 - 使用手动JSON构建确保字段正确
Integer tenantIdValue = user.getTenantId() != null ? user.getTenantId() : 0;
// 构建JSON字符串确保tenant_id字段存在且不为null
StringBuilder jsonBuilder = new StringBuilder();
jsonBuilder.append("{");
jsonBuilder.append("\"userId\":").append(user.getUserId()).append(",");
jsonBuilder.append("\"username\":\"").append(escapeJson(user.getUsername())).append("\",");
jsonBuilder.append("\"nickname\":\"").append(escapeJson(user.getNickname())).append("\",");
jsonBuilder.append("\"avatar\":\"").append(escapeJson(user.getAvatar())).append("\",");
jsonBuilder.append("\"phone\":\"").append(escapeJson(user.getPhone())).append("\",");
jsonBuilder.append("\"status\":").append(user.getStatus()).append(",");
jsonBuilder.append("\"updateTime\":\"").append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\",");
jsonBuilder.append("\"tenantId\":").append(tenantIdValue).append(","); // 驼峰格式
jsonBuilder.append("\"tenant_id\":").append(tenantIdValue); // 下划线格式
jsonBuilder.append("}");
String url = websopyBaseUrl + "/api/app/user-sync/single";
String body = jsonBuilder.toString();
log.info("同步用户到 websopy: userId={}, username={}, nickname={}, phone={}, tenantId={}, url={}",
user.getUserId(), user.getUsername(), user.getNickname(), user.getPhone(), user.getTenantId(), url);
log.info("同步用户请求体JSON: {}", body); // 改为info级别以便查看
// 额外日志tenantId 值检查
log.debug("tenantId检查 - 原始值: {}, 转换后值: {}",
user.getTenantId(), tenantIdValue);
// 发送 HTTP POST 请求
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
HttpResponse response = HttpUtils.doPost(url, "", "POST", headers, null, body);
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
log.debug("websopy 响应: {}", responseBody);
// 解析响应
JSONObject result = JSON.parseObject(responseBody);
if (result != null && result.getIntValue("code") == 0) {
log.info("用户同步成功: userId={}", user.getUserId());
} else {
String message = result != null ? result.getString("message") : "未知错误";
log.error("用户同步失败: userId={}, message={}", user.getUserId(), message);
}
} catch (Exception e) {
log.error("用户同步异常: userId={}, error={}", user.getUserId(), e.getMessage(), e);
}
} }
/** /**
* 刷新 websopy 端的用户缓存 * 刷新 websopy 端的用户缓存
* 只传 userIdwebsopy 端会通过 API 回查获取完整信息
* *
* @param userId 用户ID * @deprecated 已废弃
*/ */
@Deprecated
public void refreshUserCache(Integer userId) { public void refreshUserCache(Integer userId) {
String websopyBaseUrl = getWebsopyBaseUrl(); log.debug("UserSyncService.refreshUserCache 已废弃");
if (websopyBaseUrl == null || websopyBaseUrl.isEmpty()) {
log.warn("websopyUrl 未配置,跳过刷新缓存: userId={}", userId);
return;
}
if (userId == null) {
log.warn("userId 为空,跳过刷新");
return;
}
try {
String url = websopyBaseUrl + "/api/app/user-sync/refresh/" + userId;
log.info("刷新用户缓存: userId={}, url={}", userId, url);
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
HttpResponse response = HttpUtils.doPost(url, "", "POST", headers, null, "");
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
JSONObject result = JSON.parseObject(responseBody);
if (result != null && result.getIntValue("code") == 0) {
log.info("刷新缓存成功: userId={}", userId);
} else {
String message = result != null ? result.getString("message") : "未知错误";
log.error("刷新缓存失败: userId={}, message={}", userId, message);
}
} catch (Exception e) {
log.error("刷新缓存异常: userId={}, error={}", userId, e.getMessage(), e);
}
} }
} }

View File

@@ -111,6 +111,61 @@ public class WxService {
} }
} }
/**
* 强制刷新微信AccessToken先删除缓存再重新获取
* 用于当 token 过期或失效后,需要强制获取新 token 的场景
*
* @param tenantId 租户ID为null时使用默认值
* @return access_token
*/
public String getAccessTokenForcibly(Integer tenantId) {
if (tenantId == null) {
tenantId = 10048;
}
String key = ACCESS_TOKEN_KEY + ":" + tenantId;
// 先删除缓存
redisTemplate.delete(key);
log.info("强制刷新access_token已删除缓存: {}", key);
// 直接从微信API获取新token不再检查缓存
try {
JSONObject setting = settingService.getBySettingKey("mp-weixin");
if (setting == null) {
throw new RuntimeException("请先配置微信小程序");
}
String appId = setting.getString("appId");
String appSecret = setting.getString("appSecret");
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
throw new RuntimeException("微信小程序配置不完整");
}
// 调用微信API获取AccessToken
String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="
+ appId + "&secret=" + appSecret;
String response = HttpRequest.get(apiUrl).execute().body();
JSONObject result = JSON.parseObject(response);
String accessToken = result.getString("access_token");
if (StrUtil.isNotBlank(accessToken)) {
// 存入缓存
JSONObject tokenData = new JSONObject();
tokenData.put("access_token", accessToken);
tokenData.put("expires_in", result.get("expires_in"));
redisTemplate.opsForValue().set(key, tokenData.toJSONString(), 7000L, TimeUnit.SECONDS);
log.info("强制刷新access_token成功: {}", accessToken);
return accessToken;
} else {
throw new RuntimeException("获取AccessToken失败: " + response);
}
} catch (Exception e) {
log.error("强制刷新微信AccessToken失败: {}", e.getMessage(), e);
throw new RuntimeException("获取微信AccessToken失败: " + e.getMessage());
}
}
/** /**
* 获取微信公众号 AppID * 获取微信公众号 AppID
*/ */

View File

@@ -14,7 +14,10 @@ import com.gxwebsoft.common.system.entity.*;
import com.gxwebsoft.common.system.mapper.UserMapper; import com.gxwebsoft.common.system.mapper.UserMapper;
import com.gxwebsoft.common.system.param.LoginParam; import com.gxwebsoft.common.system.param.LoginParam;
import com.gxwebsoft.common.system.param.UserParam; import com.gxwebsoft.common.system.param.UserParam;
import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
import com.gxwebsoft.common.system.service.*; import com.gxwebsoft.common.system.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -34,6 +37,7 @@ import static com.gxwebsoft.common.core.constants.PlatformConstants.WEB;
* @author WebSoft * @author WebSoft
* @since 2018-12-24 16:10:14 * @since 2018-12-24 16:10:14
*/ */
@Slf4j
@Service @Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource @Resource
@@ -51,6 +55,9 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
@Resource @Resource
private UserRefereeService userRefereeService; private UserRefereeService userRefereeService;
@Autowired(required = false)
private SyncMessageProducer syncMessageProducer;
@Override @Override
public PageResult<User> pageRel(UserParam param) { public PageResult<User> pageRel(UserParam param) {
PageParam<User, UserParam> page = new PageParam<>(param); PageParam<User, UserParam> page = new PageParam<>(param);
@@ -130,6 +137,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
throw new BusinessException("用户角色添加失败"); throw new BusinessException("用户角色添加失败");
} }
} }
// 用户创建成功后通过MQ异步同步用户数据到 websopy
if (result && syncMessageProducer != null) {
User savedUser = getAllByUserId(String.valueOf(user.getUserId()));
if (savedUser != null) {
syncMessageProducer.sendUserSyncMessage("websopy", "CREATE", savedUser);
log.info("用户创建后发送MQ消息同步到websopy: userId={}, phone={}", user.getUserId(), user.getPhone());
}
}
return result; return result;
} }
@@ -163,6 +178,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
throw new BusinessException("用户角色添加失败"); throw new BusinessException("用户角色添加失败");
} }
} }
// 用户更新成功后通过MQ异步同步用户数据到 websopy
if (result && syncMessageProducer != null) {
User updatedUser = getAllByUserId(String.valueOf(user.getUserId()));
if (updatedUser != null) {
syncMessageProducer.sendUserSyncMessage("websopy", "UPDATE", updatedUser);
log.info("用户更新后发送MQ消息同步到websopy: userId={}, phone={}", user.getUserId(), user.getPhone());
}
}
return result; return result;
} }
@@ -323,6 +346,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
// Ensure caller (e.g. register / invite register) gets non-empty roles/authorities in response. // Ensure caller (e.g. register / invite register) gets non-empty roles/authorities in response.
addUser.setRoles(userRoleService.listByUserId(addUser.getUserId())); addUser.setRoles(userRoleService.listByUserId(addUser.getUserId()));
addUser.setAuthorities(roleMenuService.listMenuByUserId(addUser.getUserId(), null)); addUser.setAuthorities(roleMenuService.listMenuByUserId(addUser.getUserId(), null));
// addUser内部调用saveUsersaveUser已发送MQ消息这里不需要重复发送
return addUser; return addUser;
} }

View File

@@ -4,6 +4,7 @@
spring: spring:
main: main:
allow-circular-references: true allow-circular-references: true
allow-bean-definition-overriding: true # 允许bean定义覆盖解决RabbitMQConfig中的objectMapper bean冲突
datasource: datasource:
url: jdbc:mysql://47.119.165.234:13308/gxwebsoft_core?useSSL=false&serverTimezone=UTC url: jdbc:mysql://47.119.165.234:13308/gxwebsoft_core?useSSL=false&serverTimezone=UTC
username: gxwebsoft_core username: gxwebsoft_core

View File

@@ -8,6 +8,10 @@ spring:
password: jdj7HYEdYHnYEFBy password: jdj7HYEdYHnYEFBy
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
# 允许bean定义覆盖解决RabbitMQConfig和JacksonConfig中的objectMapper bean冲突
main:
allow-bean-definition-overriding: true
redis: redis:
database: 0 database: 0
host: 1Panel-redis-Q1LE host: 1Panel-redis-Q1LE

View File

@@ -12,6 +12,10 @@ spring:
application: application:
name: server name: server
# 允许bean定义覆盖解决RabbitMQConfig和JacksonConfig中的objectMapper bean冲突
main:
allow-bean-definition-overriding: true
# 连接池配置 # 连接池配置
datasource: datasource:
druid: druid:
@@ -58,6 +62,18 @@ spring:
port: 6379 port: 6379
password: redis_WSDb88 password: redis_WSDb88
# RabbitMQ 配置
rabbitmq:
host: 1Panel-rabbitmq-kvHZ
port: 5672
username: rabbitmq
password: rabbitmq
virtual-host: /
# 开启确认模式
publisher-confirm-type: correlated
# 开启Return模式
publisher-returns: true
# 邮件服务器配置 # 邮件服务器配置
mail: mail:
host: smtp.qq.com host: smtp.qq.com
@@ -111,6 +127,12 @@ config:
bucketDomain: https://oss.wsdns.cn bucketDomain: https://oss.wsdns.cn
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
# MQ同步配置
sync:
# 是否启用MQ设为false则使用原有直接同步方式
mq:
enabled: true
# JWT配置 # JWT配置
jwt: jwt:
secret: websoft-jwt-secret-key-2025-dev-environment secret: websoft-jwt-secret-key-2025-dev-environment