diff --git a/Dockerfile b/Dockerfile
index 037d30c..d8c29c2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,6 +42,14 @@ ENV KAFKA_VERSION=2.4.0
ENV KAFKA_USERNAME=kafka-external
ENV KAFKA_PASSWORD=gr58tNnlZOVGW3AmMbL2
ENV KAFKA_SASL_MECHANISM=SCRAM-SHA-512
+ENV IM_ADMIN_API_BASE_URL=http://im-server:6060
+ENV IM_ADMIN_API_SERVICE_ID=hotgo-admin
+ENV IM_ADMIN_API_SECRET=8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b
+ENV IM_INTERNAL_API_BASE_URL=http://im-server:6060
+ENV IM_INTERNAL_API_SERVICE_NAME=hotgo-admin
+ENV IM_INTERNAL_API_SECRET=8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b
+ENV IM_CONFIG_API_SERVICE_NAME=im-server
+ENV IM_CONFIG_API_SECRET=b96832c04dbfb6dd768930ec9f082ecd90d87a1ffdaa493aa070617c48de9097
COPY --from=server-build /src/manifest ./manifest
COPY --from=server-build /src/hack ./hack
diff --git a/docs/backstage/IM-后台菜单覆盖测试文档.md b/docs/backstage/IM-后台菜单覆盖测试文档.md
new file mode 100644
index 0000000..6b5f475
--- /dev/null
+++ b/docs/backstage/IM-后台菜单覆盖测试文档.md
@@ -0,0 +1,418 @@
+# IM 后台菜单覆盖测试文档
+
+## 1. 测试范围
+
+本文档用于覆盖 IM 后台菜单从「组织架构」开始,沿左侧菜单向下逐个页面点击验证。
+
+当前覆盖菜单顺序:
+
+1. 组织架构
+2. 文件治理
+3. 注册邀请码
+4. 文件策略
+5. 通知模板
+6. 工作台配置
+7. 外部服务配置
+8. 版本更新
+9. 举报原因
+10. 总经理信箱配置
+11. 意见反馈配置
+12. 任务看板
+13. Kafka 命令
+14. Kafka 死信
+15. Kafka 审计事件
+
+## 2. 测试环境
+
+| 项目 | 地址 |
+| --- | --- |
+| 前端 | `http://127.0.0.1:8001/` |
+| 后端 | `http://127.0.0.1:8000/admin` |
+| 登录账号 | 使用具备 IM 管理权限的后台账号 |
+| 浏览器 | Chrome,打开 DevTools Network 和 Console |
+
+## 3. 通用检查项
+
+每进入一个菜单页面,都先执行以下检查:
+
+| 编号 | 检查项 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| COMMON-001 | 点击左侧菜单 | URL、页签、面包屑、页面标题正确变化 | 待测 |
+| COMMON-002 | 页面首次加载 | 无白屏,无永久 loading,无明显样式错乱 | 待测 |
+| COMMON-003 | Console | 无新的 JS Error,例如组件方法不存在、字典方法不存在 | 待测 |
+| COMMON-004 | Network | 页面主列表接口 HTTP 200,业务响应 `code=0` | 待测 |
+| COMMON-005 | 查询按钮 | 点击后列表刷新,请求参数包含搜索表单值 | 待测 |
+| COMMON-006 | 重置按钮 | 搜索条件清空,列表回到默认查询 | 待测 |
+| COMMON-007 | 展开/收起 | 搜索区域展开、收起正常,不遮挡表格 | 待测 |
+| COMMON-008 | 表格刷新图标 | 点击后重新请求列表接口 | 待测 |
+| COMMON-009 | 表格密度/列设置 | 可打开设置,切换后表格仍正常显示 | 待测 |
+| COMMON-010 | 分页 | 切换页码和每页数量,请求参数正确,页面不报错 | 待测 |
+
+说明:涉及删除、撤销、重放、清理等副作用操作时,优先使用测试数据。没有测试数据时,至少验证弹窗、参数校验、取消流程;正式执行前确认不会影响线上有效数据。
+
+## 4. 组织架构
+
+菜单路径:`IM管理 / 组织架构`
+
+主要区域:部门列表、成员列表。
+
+### 4.1 搜索与列表
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| ORG-001 | 打开「组织架构」 | 页面标题显示「组织架构」,部门列表和成员列表正常渲染 | 待测 |
+| ORG-002 | 选择租户后查询部门 | 部门列表请求成功,显示部门 ID、部门名称、状态、成员数等字段 | 待测 |
+| ORG-003 | 输入部门名称、状态后点击查询 | 部门列表按条件刷新 | 待测 |
+| ORG-004 | 输入成员关键词、在职状态后点击查询 | 成员列表按条件刷新 | 待测 |
+| ORG-005 | 点击部门行「成员」 | 成员列表切换到当前部门范围,添加成员按钮可用 | 待测 |
+
+### 4.2 部门按钮
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| ORG-010 | 点击「新增部门」 | 打开部门编辑弹窗 | 待测 |
+| ORG-011 | 新增部门弹窗点击「取消」 | 弹窗关闭,列表不刷新或数据不变化 | 待测 |
+| ORG-012 | 新增部门弹窗直接点击「保存」 | 必填项校验正常展示 | 待测 |
+| ORG-013 | 填写部门名称、上级部门、排序、状态后点击「保存」 | 保存成功,部门列表刷新,新部门出现 | 待测 |
+| ORG-014 | 点击部门行「编辑」 | 打开编辑弹窗,已有数据正确回填 | 待测 |
+| ORG-015 | 修改部门信息后点击「保存」 | 保存成功,列表展示新值 | 待测 |
+| ORG-016 | 点击部门行「删除」再点取消 | 弹窗关闭,部门不删除 | 待测 |
+| ORG-017 | 点击部门行「删除」并确认 | 删除成功,列表刷新;有子部门或成员时应给出清晰错误 | 待测 |
+| ORG-018 | 点击「导出部门」 | 触发文件下载,导出条件与当前查询一致 | 待测 |
+
+### 4.3 成员按钮
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| ORG-020 | 未选择部门时观察「添加成员」 | 按钮禁用或提示先选择部门 | 待测 |
+| ORG-021 | 选择部门后点击「添加成员」 | 打开成员编辑弹窗,部门信息默认带入 | 待测 |
+| ORG-022 | 成员弹窗直接点击「保存」 | 必填项校验正常展示 | 待测 |
+| ORG-023 | 填写用户 ID、岗位、排序、可联系等信息后保存 | 保存成功,成员列表刷新 | 待测 |
+| ORG-024 | 点击成员行「编辑」 | 打开编辑弹窗,成员信息正确回填 | 待测 |
+| ORG-025 | 修改成员信息后保存 | 保存成功,列表展示新值 | 待测 |
+| ORG-026 | 点击成员行「移除」再取消 | 弹窗关闭,成员仍保留 | 待测 |
+| ORG-027 | 点击成员行「移除」并确认 | 移除成功,成员列表刷新 | 待测 |
+| ORG-028 | 点击「导出成员」 | 触发文件下载,导出条件与当前查询一致 | 待测 |
+
+## 5. 文件治理
+
+菜单路径:`IM管理 / 文件治理`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| FILE-001 | 打开页面 | 文件列表正常展示,包含文件 ID、状态、用途、上传用户、MIME、大小、引用数等字段 | 待测 |
+| FILE-002 | 按租户、文件 ID、上传用户、状态、用途、MIME、Topic、更新时间查询 | 列表按条件刷新 | 待测 |
+| FILE-003 | 点击行操作「详情」 | 打开详情弹窗,展示文件基础信息和引用列表 | 待测 |
+| FILE-004 | 详情弹窗关闭 | 弹窗正常关闭,列表状态不丢失 | 待测 |
+| FILE-005 | 点击行操作「隔离」再取消 | 确认框正常,取消后文件状态不变 | 待测 |
+| FILE-006 | 点击行操作「隔离」并确认 | 文件状态变为隔离或列表刷新后状态正确 | 待测 |
+| FILE-007 | 点击行操作「删除」再取消 | 确认框正常,取消后文件不删除 | 待测 |
+| FILE-008 | 点击行操作「删除」并确认 | 删除成功,列表刷新 | 待测 |
+| FILE-009 | 点击「清理孤儿文件」再取消 | 确认框正常,取消后不触发清理 | 待测 |
+| FILE-010 | 点击「清理孤儿文件」并确认 | 清理请求成功,页面给出成功或结果提示 | 待测 |
+
+## 6. 注册邀请码
+
+菜单路径:`IM管理 / 注册邀请码`
+
+实际菜单路由:`/#/im/registrationInvite`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| INVITE-001 | 打开页面 | 邀请码列表正常展示,包含租户、邀请码、状态、批次 ID、到期时间、使用次数等字段 | 待测 |
+| INVITE-002 | 平台管理员不选择租户点击「添加」 | 提示先选择租户 | 待测 |
+| INVITE-003 | 选择租户后点击「添加」 | 打开生成邀请码弹窗 | 待测 |
+| INVITE-004 | 生成弹窗点击「取消」 | 弹窗关闭,列表不变化 | 待测 |
+| INVITE-005 | 生成弹窗清空到期时间后点击「确认」 | 表单校验提示请选择到期时间 | 待测 |
+| INVITE-006 | 填写到期时间、备注后点击「确认」 | 生成成功,列表刷新并出现新邀请码 | 待测 |
+| INVITE-007 | 点击「批量生成」 | 打开批量生成弹窗,包含生成数量、到期时间、备注 | 待测 |
+| INVITE-008 | 批量生成数量小于 1 或大于 1000 | 表单校验拦截 | 待测 |
+| INVITE-009 | 输入合法数量后确认 | 批量生成成功,列表出现同一批次的邀请码 | 待测 |
+| INVITE-010 | 按邀请码、状态、批次 ID、创建人、创建时间、到期时间查询 | 列表按条件刷新 | 待测 |
+| INVITE-011 | 点击行操作「批次」 | 打开批次详情弹窗,展示总数、有效、撤销、过期、使用次数和明细 | 待测 |
+| INVITE-012 | 点击有效邀请码行操作「撤销」再取消 | 确认框正常,取消后状态不变 | 待测 |
+| INVITE-013 | 点击有效邀请码行操作「撤销」并确认 | 撤销成功,状态变为已撤销 | 待测 |
+| INVITE-014 | 点击有效批次行操作「撤销批次」再取消 | 确认框正常,取消后批次状态不变 | 待测 |
+| INVITE-015 | 点击有效批次行操作「撤销批次」并确认 | 批次内有效邀请码变为已撤销 | 待测 |
+| INVITE-016 | 点击「导出」 | 下载 CSV 文件,内容与当前筛选条件一致 | 待测 |
+
+## 7. 文件策略
+
+菜单路径:`IM管理 / 文件策略`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| FILE-POLICY-001 | 打开页面 | 策略列表正常展示,包含用途、大小上限、保留天数、自动清理、病毒扫描、MIME 白名单等字段 | 待测 |
+| FILE-POLICY-002 | 按租户、用途、自动清理、病毒扫描查询 | 列表按条件刷新 | 待测 |
+| FILE-POLICY-003 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
+| FILE-POLICY-004 | 编辑弹窗点击「取消」 | 弹窗关闭,数据不变化 | 待测 |
+| FILE-POLICY-005 | 修改大小上限、保留天数、开关、MIME 白名单后保存 | 保存成功,列表刷新并展示新值 | 待测 |
+| FILE-POLICY-006 | 输入非法大小或保留天数后保存 | 表单校验或后端错误提示清晰 | 待测 |
+
+## 8. 通知模板
+
+菜单路径:`IM管理 / 通知模板`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| NOTIFY-001 | 打开页面 | 模板列表正常展示,包含场景、状态、站内、Push、短信、标题模板、内容模板等字段 | 待测 |
+| NOTIFY-002 | 按租户、场景、状态查询 | 列表按条件刷新 | 待测 |
+| NOTIFY-003 | 点击行操作「编辑」 | 打开编辑弹窗,模板字段正确回填 | 待测 |
+| NOTIFY-004 | 编辑弹窗点击「取消」 | 弹窗关闭,数据不变化 | 待测 |
+| NOTIFY-005 | 修改标题模板、内容模板、站内/Push/短信开关后保存 | 保存成功,列表刷新并展示新值 | 待测 |
+| NOTIFY-006 | 清空必填字段后保存 | 表单校验或后端错误提示清晰 | 待测 |
+
+## 9. 工作台配置
+
+菜单路径:`IM管理 / 工作台配置`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| WORKBENCH-001 | 打开页面 | 工作台模块列表正常展示,包含编码、名称、路由、排序、图标 ID、角标 Key、状态、权限等字段 | 待测 |
+| WORKBENCH-002 | 按租户、编码、名称、状态查询 | 列表按条件刷新 | 待测 |
+| WORKBENCH-003 | 点击「新增模块」 | 打开模块编辑弹窗 | 待测 |
+| WORKBENCH-004 | 新增弹窗点击「取消」 | 弹窗关闭,列表不变化 | 待测 |
+| WORKBENCH-005 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
+| WORKBENCH-006 | 填写编码、名称、路由、排序、状态等信息后保存 | 保存成功,列表刷新 | 待测 |
+| WORKBENCH-007 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
+| WORKBENCH-008 | 修改模块信息后保存 | 保存成功,列表展示新值 | 待测 |
+| WORKBENCH-009 | 点击行操作「禁用」再取消 | 确认框正常,取消后状态不变 | 待测 |
+| WORKBENCH-010 | 点击行操作「禁用」并确认 | 禁用成功,列表刷新后状态正确 | 待测 |
+| WORKBENCH-011 | 点击「保存排序」 | 排序保存成功;列表刷新后顺序保持一致 | 待测 |
+
+## 10. 外部服务配置
+
+菜单路径:`IM管理 / 外部服务配置`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| EXT-CONFIG-001 | 打开页面 | 配置列表正常展示,包含服务、服务商、端点、启用、配置状态、敏感配置、检查结果等字段 | 待测 |
+| EXT-CONFIG-002 | 按租户、服务、服务商、启用状态查询 | 列表按条件刷新 | 待测 |
+| EXT-CONFIG-003 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
+| EXT-CONFIG-004 | 编辑弹窗切换「启用」开关 | 开关状态可变更,保存前不应提前提交 | 待测 |
+| EXT-CONFIG-005 | 修改服务商、端点、配置内容后保存 | 保存成功,列表刷新并展示新值 | 待测 |
+| EXT-CONFIG-006 | 点击行操作「检查」再取消 | 确认框正常,取消后不触发检查 | 待测 |
+| EXT-CONFIG-007 | 点击行操作「检查」并确认 | 检查请求成功,检查结果和检查时间刷新 | 待测 |
+
+## 11. 版本更新
+
+菜单路径:`IM管理 / 版本更新`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| RELEASE-001 | 打开页面 | 版本策略列表正常展示,包含平台、渠道、最新版本、最低支持、更新类型、下载地址等字段 | 待测 |
+| RELEASE-002 | 按租户、平台、渠道查询 | 列表按条件刷新 | 待测 |
+| RELEASE-003 | 点击「新增策略」 | 打开策略编辑弹窗 | 待测 |
+| RELEASE-004 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
+| RELEASE-005 | 填写平台、渠道、版本号、更新类型、地址、发布时间后保存 | 保存成功,列表刷新 | 待测 |
+| RELEASE-006 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
+| RELEASE-007 | 修改版本策略后保存 | 保存成功,列表展示新值 | 待测 |
+| RELEASE-008 | 输入非法版本号或 URL 后保存 | 表单校验或后端错误提示清晰 | 待测 |
+
+## 12. 举报原因
+
+菜单路径:`IM管理 / 举报原因`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| ABUSE-001 | 打开页面 | 原因列表正常展示,包含对象、编码、文案、要求描述、允许证据、排序、状态等字段 | 待测 |
+| ABUSE-002 | 按租户、对象、编码、状态查询 | 列表按条件刷新 | 待测 |
+| ABUSE-003 | 点击「新增原因」 | 打开原因编辑弹窗 | 待测 |
+| ABUSE-004 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
+| ABUSE-005 | 填写对象、编码、文案、排序、状态等信息后保存 | 保存成功,列表刷新 | 待测 |
+| ABUSE-006 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
+| ABUSE-007 | 修改原因配置后保存 | 保存成功,列表展示新值 | 待测 |
+| ABUSE-008 | 点击行操作「禁用」再取消 | 确认框正常,取消后状态不变 | 待测 |
+| ABUSE-009 | 点击行操作「禁用」并确认 | 禁用成功,列表刷新后状态正确 | 待测 |
+
+## 13. 总经理信箱配置
+
+菜单路径:`IM管理 / 总经理信箱配置`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| MAILBOX-001 | 打开页面 | 配置表单正常展示,租户选择框可用 | 待测 |
+| MAILBOX-002 | 选择租户 | 自动加载该租户信箱配置 | 待测 |
+| MAILBOX-003 | 点击「刷新」 | 当前租户配置重新加载,表单无异常 | 待测 |
+| MAILBOX-004 | 未选择租户时观察「保存」 | 按钮禁用或提示先选择租户 | 待测 |
+| MAILBOX-005 | 修改启用状态、接收人、处理策略等配置后点击「保存」 | 保存成功,再次刷新后配置保持一致 | 待测 |
+| MAILBOX-006 | 输入非法配置后保存 | 表单校验或后端错误提示清晰 | 待测 |
+
+## 14. 意见反馈配置
+
+菜单路径:`IM管理 / 意见反馈配置`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| FEEDBACK-001 | 打开页面 | 配置表单正常展示,租户选择框可用 | 待测 |
+| FEEDBACK-002 | 选择租户 | 自动加载该租户反馈配置 | 待测 |
+| FEEDBACK-003 | 点击「刷新」 | 当前租户配置重新加载,表单无异常 | 待测 |
+| FEEDBACK-004 | 未选择租户时观察「保存」 | 按钮禁用或提示先选择租户 | 待测 |
+| FEEDBACK-005 | 修改启用状态、分类、处理人、限制策略等配置后点击「保存」 | 保存成功,再次刷新后配置保持一致 | 待测 |
+| FEEDBACK-006 | 输入非法配置后保存 | 表单校验或后端错误提示清晰 | 待测 |
+
+## 15. 任务看板
+
+菜单路径:`IM管理 / 任务看板`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| TASK-001 | 打开页面 | 看板列表正常展示,包含租户、企业码、状态、DB、业务待投递、业务失败、群待投递、群失败、孤儿文件、待物理清理等字段 | 待测 |
+| TASK-002 | 按租户、状态查询 | 列表按条件刷新 | 待测 |
+| TASK-003 | 查看建议动作字段 | 异常租户展示可理解的建议动作;正常租户展示正常或为空 | 待测 |
+| TASK-004 | 刷新表格 | 统计数据重新加载,无接口报错 | 待测 |
+
+## 16. Kafka 命令
+
+菜单路径:`IM管理 / Kafka命令`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| KAFKA-CMD-001 | 打开页面 | 命令列表正常展示,包含租户、命令 ID、事件类型、业务类型、业务 ID、状态、错误码、消息等字段 | 待测 |
+| KAFKA-CMD-002 | 按租户、命令 ID、事件 ID、事件类型、业务 ID、状态、创建时间查询 | 列表按条件刷新 | 待测 |
+| KAFKA-CMD-003 | 点击「发送缓存失效测试」 | 请求成功,新增一条缓存失效命令或页面提示成功 | 待测 |
+| KAFKA-CMD-004 | 点击「发送工作台测试事件」 | 请求成功,新增一条工作台测试事件命令或页面提示成功 | 待测 |
+| KAFKA-CMD-005 | 点击行操作「详情」 | 打开详情弹窗,展示命令 payload、结果、错误原因等信息 | 待测 |
+| KAFKA-CMD-006 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
+| KAFKA-CMD-007 | 发送测试事件后等待消费结果 | 状态从待发送/已发送变为成功或失败,失败时错误信息可读 | 待测 |
+
+## 17. Kafka 死信
+
+菜单路径:`IM管理 / Kafka死信`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| DEAD-001 | 打开页面 | 死信列表正常展示,包含死信 ID、事件 ID、来源 Topic、消费组、事件类型、业务类型、业务 ID、状态等字段 | 待测 |
+| DEAD-002 | 按租户、状态、事件类型、业务类型、业务 ID 查询 | 列表按条件刷新 | 待测 |
+| DEAD-003 | 点击行操作「详情」 | 打开详情弹窗,展示死信 payload、错误原因、处理信息 | 待测 |
+| DEAD-004 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
+| DEAD-005 | 详情弹窗点击「忽略」 | 操作成功,状态变为已忽略或列表刷新后状态正确 | 待测 |
+| DEAD-006 | 详情弹窗点击「标记已处理」 | 操作成功,状态变为已处理 | 待测 |
+| DEAD-007 | 详情弹窗点击「重放」 | 操作成功,重放次数增加,状态或处理结果正确更新 | 待测 |
+| DEAD-008 | 对已处理、已忽略死信重复操作 | 系统应拦截或给出清晰提示,不出现脏状态 | 待测 |
+
+## 18. Kafka 审计事件
+
+菜单路径:`IM管理 / Kafka审计事件`
+
+| 编号 | 操作 | 预期结果 | 结果 |
+| --- | --- | --- | --- |
+| KAFKA-AUDIT-001 | 打开页面 | 审计事件列表正常展示,包含租户、事件 ID、事件类型、动作、资源、资源 ID、操作者、结果等字段 | 待测 |
+| KAFKA-AUDIT-002 | 按租户编码、事件 ID、事件类型、动作、资源、资源 ID、结果、接收时间查询 | 列表按条件刷新 | 待测 |
+| KAFKA-AUDIT-003 | 点击行操作「详情」 | 打开详情弹窗,展示事件详情、请求上下文、结果信息 | 待测 |
+| KAFKA-AUDIT-004 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
+
+## 19. 测试数据建议
+
+| 数据类型 | 建议准备 |
+| --- | --- |
+| 租户 | 至少 1 个正常租户,具备独立业务库 |
+| 组织架构 | 至少 1 个根部门、1 个子部门、2 个用户成员 |
+| 文件治理 | 至少 1 个正常文件、1 个可隔离文件、1 个孤儿文件 |
+| 注册邀请码 | 至少 1 个有效邀请码、1 个过期邀请码、1 个批次邀请码 |
+| Kafka | Kafka、消费者、IM Server 正常运行;准备 1 条可产生成功结果的命令和 1 条可产生死信的命令 |
+| 配置类 | 文件策略、通知模板、工作台、外部服务、版本更新、举报原因均有初始数据 |
+
+## 20. 缺陷记录模板
+
+| 缺陷编号 | 菜单 | 操作步骤 | 实际结果 | 预期结果 | Console/Network 信息 | 严重级别 | 状态 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| BUG-001 | | | | | | | 待处理 |
+
+## 21. 第一轮执行记录
+
+执行时间:2026-09-03 15:16 - 15:22
+
+执行环境:
+
+| 项目 | 值 |
+| --- | --- |
+| 前端 | `http://127.0.0.1:8001/` |
+| 后端 | Docker 容器 `im-local-admin`,端口 `8000` |
+| MySQL | Docker 容器 `relaxed_cori`,库 `hotgo`、`tinode`、`tinode_test` |
+| Redis | Docker 容器 `brave_blackburn` |
+| Kafka | Docker 容器 `kafka` |
+
+本轮说明:
+
+1. 从左侧菜单「组织架构」开始,按菜单顺序逐页点击。
+2. 对查询、重置、展开、刷新、新增、编辑、详情等非破坏性按钮进行了点击验证。
+3. 对删除、隔离、撤销、禁用、重放等有副作用的按钮,本轮只验证入口和取消流程,不确认执行。
+4. 「收起」按钮在自动化脚本里先于「展开」检测,产生过误报,本轮不作为缺陷记录。
+5. 「注册邀请码」菜单点击正常,真实路由为 `/#/im/registrationInvite`;直接访问 `/#/im/registrationInvite/index` 会进入 ErrorPage,测试文档已修正。
+
+### 21.1 页面通过项
+
+| 菜单 | 本轮结果 |
+| --- | --- |
+| 组织架构 | 页面可打开,查询/重置/展开/新增部门可点击;添加成员因未先选择部门处于禁用,符合预期 |
+| 文件治理 | 页面可打开,查询/重置/展开/详情可点击;列表有数据 |
+| 注册邀请码 | 通过左侧菜单可打开,查询/重置/展开/添加/批量生成/导出可见;列表有 2 条历史数据 |
+| 工作台配置 | 页面可打开,查询/重置/展开/新增模块/编辑可点击;禁用弹窗可打开并取消 |
+| 版本更新 | 页面可打开,查询/重置/展开/新增策略/编辑可点击;列表有数据 |
+| 举报原因 | 页面可打开,查询/重置/展开/新增原因/编辑可点击;禁用弹窗可打开并取消 |
+| 总经理信箱配置 | 页面可打开,保存/刷新按钮可见,刷新可点击 |
+| 意见反馈配置 | 页面可打开,保存/刷新按钮可见,刷新可点击 |
+| 任务看板 | 页面可打开,查询/重置/展开可点击 |
+
+### 21.2 本轮发现的问题
+
+| 缺陷编号 | 菜单 | 操作步骤 | 实际结果 | 预期结果 | 证据 | 严重级别 | 状态 |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| BUG-IM-001 | 文件策略 | 点击「IM管理 / 文件策略」,或点击查询/重置 | 页面 Console 报 `检查IM文件策略失败`,列表无法加载,行操作「编辑」不可测 | 文件策略列表应正常加载,缺省策略应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_file_policy ... Error 1146: Table 'hotgo.im_file_policy' doesn't exist`;代码位置:`server/internal/logic/sys/im_file_policy.go:128` | P0 | 待处理 |
+| BUG-IM-002 | 通知模板 | 点击「IM管理 / 通知模板」,或点击查询/重置 | 页面 Console 报 `检查IM通知模板失败,请稍后重试!`,列表无法加载,行操作「编辑」不可测 | 通知模板列表应正常加载,缺省模板应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_notification_template ... Error 1146: Table 'hotgo.im_notification_template' doesn't exist`;代码位置:`server/internal/logic/sys/im_notification.go:123` | P0 | 待处理 |
+| BUG-IM-003 | 外部服务配置 | 点击「IM管理 / 外部服务配置」,或点击查询/重置 | 页面 Console 报 `初始化IM外部服务配置失败`,列表无法加载,行操作「编辑」「检查」不可测 | 外部服务配置列表应正常加载,缺省服务配置应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_external_config ... Error 1146: Table 'hotgo.im_external_config' doesn't exist`;代码位置:`server/internal/logic/sys/im_external_config.go:194` | P0 | 待处理 |
+| BUG-IM-004 | Kafka命令 | 点击「IM管理 / Kafka命令」后点击「发送缓存失效测试」 | 页面 toast 显示 `读取IM命令Outbox失败`,Kafka 命令列表仍为 0 条 | 应写入 Outbox 并投递 Kafka,页面能看到新事件或明确状态 | 后端日志:`读取IM命令Outbox失败: element of parameter "pointer" for function Scan should type of struct/*struct/[]struct/[]*struct`;代码位置:`server/internal/library/imkafka/command.go:103` | P0 | 待处理 |
+| BUG-IM-005 | Kafka命令 | 点击「发送缓存失效测试」「发送工作台测试事件」后查看数据库 | 正确表 `hotgo.im_admin_outbox` 为 0 条;带前缀表 `hotgo.hg_im_admin_outbox` 新增 3 条 pending 记录 | Admin 新增 IM 表和代码查询应统一使用无前缀 `im_*` 表,避免 `hg_im_*` 和 `im_*` 双表分裂 | MySQL:`SHOW TABLES LIKE 'hg_im_%'` 可见 `hg_im_admin_outbox`、`hg_im_admin_commands`、`hg_im_audit_events`;`hg_im_admin_outbox` 有 pending 数据,`im_admin_outbox` 为空 | P0 | 待处理 |
+| BUG-IM-006 | Kafka死信 | 点击「IM管理 / Kafka死信」 | 页面可打开,但列表无数据,行操作「详情」「忽略」「标记已处理」「重放」无法覆盖 | 联调测试库应准备至少 1 条死信数据,才能覆盖处置按钮 | MySQL:`hotgo.im_kafka_dead_letter_events` 计数为 0 | P1 | 待补测试数据 |
+| BUG-IM-007 | Kafka审计事件 | 点击「IM管理 / Kafka审计事件」 | 页面可打开,但列表无数据,行操作「详情」无法覆盖 | 联调测试库应准备至少 1 条审计事件数据,才能覆盖详情按钮 | MySQL:`hotgo.im_audit_events` 计数为 0 | P1 | 待补测试数据 |
+
+### 21.3 数据库核查结果
+
+| 库 | 表 | 结果 |
+| --- | --- | --- |
+| `hotgo` | `im_file_policy` | 不存在 |
+| `hotgo` | `im_notification_template` | 不存在 |
+| `hotgo` | `im_external_config` | 不存在 |
+| `hotgo` | `im_admin_commands` | 存在,0 条 |
+| `hotgo` | `im_admin_outbox` | 存在,0 条 |
+| `hotgo` | `hg_im_admin_outbox` | 存在,3 条 pending |
+| `hotgo` | `im_kafka_dead_letter_events` | 存在,0 条 |
+| `hotgo` | `im_audit_events` | 存在,0 条 |
+
+### 21.4 下一步建议
+
+1. 先导入缺失表 SQL:`server/storage/data/generate/im_file_policy_menu.sql`、`server/storage/data/generate/im_notification_menu.sql`、`server/storage/data/generate/im_external_config_menu.sql`。
+2. 修复 Kafka Outbox 表名前缀分裂问题,确保所有 Kafka Admin 表统一访问 `im_*`,不要落到 `hg_im_*`。
+3. 修复 `DispatchOutboxByEvent` 中 `g.DB().GetOne` 触发的 `Scan` 参数类型错误。
+4. 准备 Kafka 死信和 Kafka 审计事件测试数据后,重新覆盖「详情」「忽略」「标记已处理」「重放」等按钮。
+5. 上述 P0 处理后,重新执行本测试文档第 4 到第 18 章。
+
+## 22. 第二轮修复记录
+
+执行时间:2026-09-03 15:37 - 15:50
+
+### 22.1 已处理项
+
+| 缺陷编号 | 处理结果 | 复测结果 |
+| --- | --- | --- |
+| BUG-IM-001 | 已补齐 `hotgo.im_file_policy` 表;访问文件策略列表时可自动初始化默认策略 | `/admin/imFilePolicy/list?tenantId=1` 返回 `code=0`,列表 6 条 |
+| BUG-IM-002 | 已补齐 `hotgo.im_notification_template` 表;访问通知模板列表时可自动初始化默认模板 | `/admin/imNotification/templateList` 返回 `code=0`,列表 7 条 |
+| BUG-IM-003 | 已补齐 `hotgo.im_external_config` 表;访问外部服务配置列表时可自动初始化默认配置 | `/admin/imExternalConfig/list?tenantId=1` 返回 `code=0`,列表 5 条 |
+| BUG-IM-004 | 已修复 Kafka Admin 命令/Outbox 读写 DB,统一改为无前缀 `imRegistry` 连接 | 点击等价接口 `/admin/imKafkaCommand/cacheInvalidate` 返回 `code=0`,不再报 `读取IM命令Outbox失败` |
+| BUG-IM-005 | 已修复 `im_admin_commands`、`im_admin_outbox`、`im_audit_events`、`im_kafka_dead_letter_events` 读写前缀分裂 | 新增 Kafka 测试事件写入 `hotgo.im_admin_outbox`,状态为 `sent`;没有继续写入 `hg_im_admin_outbox` |
+
+### 22.2 额外修复
+
+| 问题 | 处理结果 | 复测结果 |
+| --- | --- | --- |
+| Docker 本地 admin 缺少运行时 MySQL/Redis 环境变量导致启动 panic | 本地容器启动时显式传入 `MYSQL_DSN`、`REDIS_ADDR`、`TOKEN_SECRET_KEY` 等变量 | `im-local-admin` 正常启动,`http://127.0.0.1:8000/admin/` 可访问 |
+| admin 容器内 `im-server:6060` 解析到不可用地址,死信内部查询 EOF | `config.template`、`entrypoint.sh` 增加 IM API 环境变量;本地容器使用 `IM_INTERNAL_API_BASE_URL=http://im-local-im:6060` | `/admin/imDeadLetter/list?tenantId=1` 返回 `code=0`,列表 18 条 |
+| 测试库 IM 菜单中文标题乱码 | 已在测试库用 UTF-8 连接修复 `hg_admin_menu.title`;新增 `im_menu_title_repair.sql` 可重复修复 | `imManager`、`imOrgIndex`、`imFilePolicyIndex`、`imKafkaCommandIndex` 等标题查询显示正常中文 |
+| 部分新增菜单 SQL 使用旧版 `hg_admin_menu` 字段 `route/is_cache/created_by/updated_by` | 已转换相关 `*_menu.sql` 菜单插入字段为当前 HotGo 表结构 | 相关 SQL 在本地 `hotgo` 库重复执行通过,无 `Unknown column 'route'` |
+
+### 22.3 当前剩余注意项
+
+1. 当前浏览器页签仍保存旧 token,页面上可能继续弹「登录身份已失效」;刷新并重新登录后再继续 UI 点击测试。
+2. `hg_im_admin_outbox` 里还保留第一轮测试误写入的 3 条旧数据,仅用于证明旧问题,不影响新链路;后续可按运维需要清理。
+3. Kafka 审计事件列表当前仍无数据,需要 IM Server 产生或导入审计事件后才能覆盖详情弹窗。
diff --git a/server/internal/library/imkafka/command.go b/server/internal/library/imkafka/command.go
index 2cfcf85..5b81861 100644
--- a/server/internal/library/imkafka/command.go
+++ b/server/internal/library/imkafka/command.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"hotgo/internal/consts"
+ "hotgo/internal/library/imtenant"
"hotgo/internal/library/queue"
"time"
@@ -60,7 +61,7 @@ func PublishAdminCommand(ctx context.Context, in AdminCommandInput) (res *AdminC
return nil, gerror.Wrap(err, "序列化IM命令Envelope失败")
}
- db := g.DB()
+ db := adminDB()
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
nowTime := gtime.Now()
if _, err := tx.Exec(`INSERT INTO im_admin_commands
@@ -101,7 +102,7 @@ func PublishAdminCommand(ctx context.Context, in AdminCommandInput) (res *AdminC
}
func DispatchOutboxByEvent(ctx context.Context, eventID string) error {
- row, err := g.DB().GetOne(ctx, `SELECT * FROM im_admin_outbox WHERE event_id=? AND status=? LIMIT 1`,
+ row, err := adminDB().GetOne(ctx, `SELECT * FROM im_admin_outbox WHERE event_id=? AND status=? LIMIT 1`,
eventID, consts.ImAdminOutboxStatusPending)
if err != nil {
return gerror.Wrap(err, "读取IM命令Outbox失败")
@@ -146,7 +147,7 @@ func PublishConfigEvent(ctx context.Context, in ConfigEventInput) error {
return gerror.Wrap(err, "序列化IM配置事件Envelope失败")
}
nowTime := gtime.Now()
- _, err = g.DB().Exec(ctx, `INSERT INTO im_admin_outbox
+ _, err = adminDB().Exec(ctx, `INSERT INTO im_admin_outbox
(event_id,command_id,topic,event_type,biz_type,biz_id,key_value,envelope,status,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),?,?,?)`,
eventID, "", consts.QueueImConfigEventTopic, in.EventType, "config", in.BizID, in.TenantCode,
@@ -198,7 +199,7 @@ func PublishEvent(ctx context.Context, in EventInput) (eventID string, err error
return "", gerror.Wrap(err, "序列化IM Kafka事件Envelope失败")
}
nowTime := gtime.Now()
- _, err = g.DB().Exec(ctx, `INSERT INTO im_admin_outbox
+ _, err = adminDB().Exec(ctx, `INSERT INTO im_admin_outbox
(event_id,command_id,topic,event_type,biz_type,biz_id,key_value,envelope,status,created_at,updated_at)
VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),?,?,?)`,
eventID, "", in.Topic, in.EventType, in.BizType, in.BizID, in.BizID,
@@ -216,7 +217,7 @@ func DispatchPendingOutbox(ctx context.Context, limit int) (sent int, err error)
if limit <= 0 {
limit = 100
}
- rows, err := g.DB().GetAll(ctx, `SELECT * FROM im_admin_outbox
+ rows, err := adminDB().GetAll(ctx, `SELECT * FROM im_admin_outbox
WHERE status IN (?,?) AND (next_retry_at IS NULL OR next_retry_at <= NOW())
ORDER BY id ASC LIMIT ?`, consts.ImAdminOutboxStatusPending, consts.ImAdminOutboxStatusFailed, limit)
if err != nil {
@@ -250,14 +251,14 @@ func dispatchOutboxRecord(ctx context.Context, row gdb.Record) error {
return err
}
now := gtime.Now()
- _, err := g.DB().Exec(ctx, `UPDATE im_admin_outbox
+ _, err := adminDB().Exec(ctx, `UPDATE im_admin_outbox
SET status=?,attempts=attempts+1,sent_at=?,last_reason='',last_error='',updated_at=?
WHERE event_id=?`, consts.ImAdminOutboxStatusSent, now, now, eventID)
if err != nil {
return gerror.Wrap(err, "更新IM命令Outbox投递状态失败")
}
if commandID != "" {
- _, err = g.DB().Exec(ctx, `UPDATE im_admin_commands
+ _, err = adminDB().Exec(ctx, `UPDATE im_admin_commands
SET status=?,sent_at=?,updated_at=? WHERE command_id=?`,
consts.ImAdminCommandStatusRunning, now, now, commandID)
if err != nil {
@@ -269,7 +270,7 @@ func dispatchOutboxRecord(ctx context.Context, row gdb.Record) error {
func markOutboxFailed(ctx context.Context, eventID string, reason string, message string) error {
now := gtime.Now()
- _, err := g.DB().Exec(ctx, `UPDATE im_admin_outbox
+ _, err := adminDB().Exec(ctx, `UPDATE im_admin_outbox
SET status=?,attempts=attempts+1,next_retry_at=?,last_reason=?,last_error=?,updated_at=?
WHERE event_id=?`, consts.ImAdminOutboxStatusFailed, now.Add(30*time.Second), reason, message, now, eventID)
if err != nil {
@@ -316,9 +317,13 @@ func ApplyCommandResult(ctx context.Context, result CommandResult) error {
}
sql += ` WHERE ` + whereColumn + `=?`
args = append(args, whereValue)
- _, err := g.DB().Exec(ctx, sql, args...)
+ _, err := adminDB().Exec(ctx, sql, args...)
if err != nil {
return gerror.Wrap(err, "更新IM后台命令结果失败")
}
return nil
}
+
+func adminDB() gdb.DB {
+ return imtenant.RegistryDB()
+}
diff --git a/server/internal/logic/sys/im_kafka_audit.go b/server/internal/logic/sys/im_kafka_audit.go
index 1e187a8..59f7bf2 100644
--- a/server/internal/logic/sys/im_kafka_audit.go
+++ b/server/internal/logic/sys/im_kafka_audit.go
@@ -2,12 +2,12 @@ package sys
import (
"context"
+ "hotgo/internal/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
- "github.com/gogf/gf/v2/frame/g"
)
type sSysImKafkaAudit struct{}
@@ -25,7 +25,8 @@ func (s *sSysImKafkaAudit) List(ctx context.Context, in *sysin.ImKafkaAuditListI
return
}
where, args := buildImKafkaAuditWhere(in)
- totalValue, err := g.DB().GetValue(ctx, "SELECT COUNT(*) FROM im_audit_events "+where, args...)
+ db := imtenant.RegistryDB()
+ totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_audit_events "+where, args...)
if err != nil {
err = gerror.Wrap(err, "获取IM Kafka审计事件总数失败,请稍后重试!")
return
@@ -45,7 +46,7 @@ func (s *sSysImKafkaAudit) List(ctx context.Context, in *sysin.ImKafkaAuditListI
action,resource,resource_id,result,reason,occurred_at,received_at
FROM im_audit_events ` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?`
queryArgs := append(append([]any{}, args...), perPage, offset)
- rows, err := g.DB().GetAll(ctx, query, queryArgs...)
+ rows, err := db.GetAll(ctx, query, queryArgs...)
if err != nil {
err = gerror.Wrap(err, "获取IM Kafka审计事件列表失败,请稍后重试!")
return
@@ -126,7 +127,7 @@ func (s *sSysImKafkaAudit) View(ctx context.Context, in *sysin.ImKafkaAuditViewI
args = append(args, in.EventId)
}
query += " LIMIT 1"
- row, err := g.DB().GetOne(ctx, query, args...)
+ row, err := imtenant.RegistryDB().GetOne(ctx, query, args...)
if err != nil {
return nil, gerror.Wrap(err, "获取IM Kafka审计事件详情失败,请稍后重试!")
}
diff --git a/server/internal/logic/sys/im_kafka_command.go b/server/internal/logic/sys/im_kafka_command.go
index 85c3495..ae97609 100644
--- a/server/internal/logic/sys/im_kafka_command.go
+++ b/server/internal/logic/sys/im_kafka_command.go
@@ -10,12 +10,12 @@ import (
"hotgo/internal/consts"
"hotgo/internal/library/contexts"
"hotgo/internal/library/imkafka"
+ "hotgo/internal/library/imtenant"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
"strings"
"github.com/gogf/gf/v2/errors/gerror"
- "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
@@ -34,7 +34,8 @@ func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandL
return
}
where, args := buildImKafkaCommandWhere(in)
- totalValue, err := g.DB().GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...)
+ db := imtenant.RegistryDB()
+ totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_admin_commands "+where, args...)
if err != nil {
err = gerror.Wrap(err, "获取IM后台命令总数失败,请稍后重试!")
return
@@ -53,7 +54,7 @@ func (s *sSysImKafkaCommand) List(ctx context.Context, in *sysin.ImKafkaCommandL
status,reason,message,sent_at,completed_at,created_by,created_at,updated_at
FROM im_admin_commands ` + where + ` ORDER BY id DESC LIMIT ? OFFSET ?`
queryArgs := append(append([]any{}, args...), perPage, offset)
- rows, err := g.DB().GetAll(ctx, query, queryArgs...)
+ rows, err := db.GetAll(ctx, query, queryArgs...)
if err != nil {
err = gerror.Wrap(err, "获取IM后台命令列表失败,请稍后重试!")
return
@@ -80,7 +81,7 @@ func (s *sSysImKafkaCommand) View(ctx context.Context, in *sysin.ImKafkaCommandV
args = append(args, in.CommandId)
}
query += " LIMIT 1"
- row, err := g.DB().GetOne(ctx, query, args...)
+ row, err := imtenant.RegistryDB().GetOne(ctx, query, args...)
if err != nil {
return nil, gerror.Wrap(err, "获取IM后台命令详情失败,请稍后重试!")
}
diff --git a/server/internal/logic/sys/im_org.go b/server/internal/logic/sys/im_org.go
index 8ceb6d4..c625f5e 100644
--- a/server/internal/logic/sys/im_org.go
+++ b/server/internal/logic/sys/im_org.go
@@ -9,6 +9,7 @@ import (
"hotgo/internal/service"
"hotgo/utility/convert"
"hotgo/utility/excel"
+ "net/url"
"strings"
"github.com/gogf/gf/v2/database/gdb"
@@ -34,33 +35,28 @@ func (s *sSysImOrg) DepartmentList(ctx context.Context, in *sysin.ImOrgDepartmen
if err = in.Filter(ctx); err != nil {
return
}
- db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
+ tenant, err := resolveImAdminTenant(ctx, in.TenantId)
if err != nil {
return
}
- mod := s.departmentModel(ctx, db)
+ values := addPageQuery(url.Values{}, in.Page, in.PerPage)
if in.DepartmentId != "" {
- mod = mod.Where("d.department_id", strings.TrimSpace(in.DepartmentId))
+ values.Set("department_id", strings.TrimSpace(in.DepartmentId))
}
if in.ParentId != "" {
- mod = mod.Where("d.parent_id", strings.TrimSpace(in.ParentId))
+ values.Set("parent_id", strings.TrimSpace(in.ParentId))
}
if in.Name != "" {
- mod = mod.WhereLike("d.name", "%"+strings.TrimSpace(in.Name)+"%")
+ values.Set("name", strings.TrimSpace(in.Name))
}
if in.State != "" {
- mod = mod.Where("d.state", strings.TrimSpace(in.State))
+ values.Set("state", strings.TrimSpace(in.State))
}
- mod = mod.Page(in.Page, in.PerPage).OrderAsc("d.sort_order").OrderAsc("d.id")
- if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
- err = gerror.Wrap(err, "获取IM组织部门失败,请稍后重试!")
- return
- }
- for _, item := range list {
- item.TenantId = tenant.Id
- item.TenantName = tenant.Name
- }
- return
+ return imAdminQueryList[sysin.ImOrgDepartmentModel](ctx, tenant,
+ "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/org/departments",
+ values,
+ nil,
+ )
}
func (s *sSysImOrg) DepartmentExport(ctx context.Context, in *sysin.ImOrgDepartmentListInp) (err error) {
@@ -195,34 +191,28 @@ func (s *sSysImOrg) MemberList(ctx context.Context, in *sysin.ImOrgMemberListInp
if err = in.Filter(ctx); err != nil {
return
}
- db, tenant, err := resolveImTenantDB(ctx, in.TenantId)
+ tenant, err := resolveImAdminTenant(ctx, in.TenantId)
if err != nil {
return
}
- mod := s.memberModel(ctx, db)
+ values := addPageQuery(url.Values{}, in.Page, in.PerPage)
if in.DepartmentId != "" {
- mod = mod.Where("m.department_id", strings.TrimSpace(in.DepartmentId))
+ values.Set("department_id", strings.TrimSpace(in.DepartmentId))
}
if in.UserId > 0 {
- mod = mod.Where("m.user_id", in.UserId)
+ values.Set("user_id", int64ToString(in.UserId))
}
if in.EmploymentState != "" {
- mod = mod.Where("m.employment_state", strings.TrimSpace(in.EmploymentState))
+ values.Set("employment_state", strings.TrimSpace(in.EmploymentState))
}
- if q := strings.TrimSpace(in.Query); q != "" {
- like := "%" + q + "%"
- mod = mod.Where("(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?)", like, like, like, like)
+ if in.Query != "" {
+ values.Set("q", strings.TrimSpace(in.Query))
}
- mod = mod.Page(in.Page, in.PerPage).OrderAsc("m.sort_order").OrderAsc("m.id")
- if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
- err = gerror.Wrap(err, "获取IM组织成员失败,请稍后重试!")
- return
- }
- for _, item := range list {
- item.TenantId = tenant.Id
- item.TenantName = tenant.Name
- }
- return
+ return imAdminQueryList[sysin.ImOrgMemberModel](ctx, tenant,
+ "/internal/admin-query/tenants/"+url.PathEscape(tenant.Code)+"/org/members",
+ values,
+ nil,
+ )
}
func (s *sSysImOrg) MemberExport(ctx context.Context, in *sysin.ImOrgMemberListInp) (err error) {
diff --git a/server/internal/queues/im_audit_event.go b/server/internal/queues/im_audit_event.go
index dadb6b4..7b2726a 100644
--- a/server/internal/queues/im_audit_event.go
+++ b/server/internal/queues/im_audit_event.go
@@ -5,9 +5,9 @@ import (
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/imkafka"
+ "hotgo/internal/library/imtenant"
"hotgo/internal/library/queue"
- "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
@@ -32,7 +32,7 @@ func (q *qImAuditEvent) Handle(ctx context.Context, mqMsg queue.MqMsg) (err erro
if len(envelope.Payload) > 0 {
_ = json.Unmarshal(envelope.Payload, &payload)
}
- _, err = g.DB().Exec(ctx, `INSERT IGNORE INTO im_audit_events
+ _, err = imtenant.RegistryDB().Exec(ctx, `INSERT IGNORE INTO im_audit_events
(event_id,tenant_code,event_type,biz_type,biz_id,actor_type,actor_id,action,resource,resource_id,result,reason,payload,envelope,occurred_at,received_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,CAST(NULLIF(?, '') AS JSON),CAST(? AS JSON),?,?)`,
envelope.EventID, envelope.TenantCode, envelope.EventType, envelope.BizType, envelope.BizID,
diff --git a/server/internal/queues/im_dead_letter.go b/server/internal/queues/im_dead_letter.go
index b1dd6fc..616cf23 100644
--- a/server/internal/queues/im_dead_letter.go
+++ b/server/internal/queues/im_dead_letter.go
@@ -5,9 +5,9 @@ import (
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/imkafka"
+ "hotgo/internal/library/imtenant"
"hotgo/internal/library/queue"
- "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
@@ -32,7 +32,7 @@ func (q *qImDeadLetter) Handle(ctx context.Context, mqMsg queue.MqMsg) (err erro
if len(envelope.Payload) > 0 {
_ = json.Unmarshal(envelope.Payload, &payload)
}
- _, err = g.DB().Exec(ctx, `INSERT IGNORE INTO im_kafka_dead_letter_events
+ _, err = imtenant.RegistryDB().Exec(ctx, `INSERT IGNORE INTO im_kafka_dead_letter_events
(event_id,parent_event_id,tenant_code,event_type,biz_type,biz_id,source_service,target_service,dead_id,reason,message,payload,envelope,received_at)
VALUES(?,?,?,?,?,?,?,?,?,?,?,CAST(NULLIF(?, '') AS JSON),CAST(? AS JSON),?)`,
envelope.EventID, envelope.ParentEventID, envelope.TenantCode, envelope.EventType, envelope.BizType, envelope.BizID,
diff --git a/server/manifest/config/config.template b/server/manifest/config/config.template
index 9ed23d2..7d6a9f6 100644
--- a/server/manifest/config/config.template
+++ b/server/manifest/config/config.template
@@ -306,17 +306,17 @@ hggen:
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
imAdminApi:
- baseUrl: "http://im-server:6060"
- serviceId: "hotgo-admin"
- serviceSecret: "8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b"
+ baseUrl: "$IM_ADMIN_API_BASE_URL"
+ serviceId: "$IM_ADMIN_API_SERVICE_ID"
+ serviceSecret: "$IM_ADMIN_API_SECRET"
timeout: "5s"
imInternalApi:
- baseUrl: "http://im-server:6060"
- serviceName: "hotgo-admin"
- serviceSecret: "8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b"
+ baseUrl: "$IM_INTERNAL_API_BASE_URL"
+ serviceName: "$IM_INTERNAL_API_SERVICE_NAME"
+ serviceSecret: "$IM_INTERNAL_API_SECRET"
imConfigApi:
- serviceName: "im-server"
- serviceSecret: "b96832c04dbfb6dd768930ec9f082ecd90d87a1ffdaa493aa070617c48de9097"
+ serviceName: "$IM_CONFIG_API_SERVICE_NAME"
+ serviceSecret: "$IM_CONFIG_API_SECRET"
timeWindow: "300s"
diff --git a/server/manifest/docker/entrypoint.sh b/server/manifest/docker/entrypoint.sh
index 1828001..72a473a 100644
--- a/server/manifest/docker/entrypoint.sh
+++ b/server/manifest/docker/entrypoint.sh
@@ -6,10 +6,11 @@ cd /app
# Render the real config from the template using only the specific env vars we
# expect — avoids accidentally substituting anything else that might contain a
# literal '$' in the YAML.
-envsubst '$MYSQL_DSN $REDIS_ADDR $REDIS_DB $REDIS_PASSWORD $TOKEN_SECRET_KEY $QUEUE_DRIVER $KAFKA_ADDRESS $KAFKA_VERSION $KAFKA_USERNAME $KAFKA_PASSWORD $KAFKA_SASL_MECHANISM' \
+envsubst '$MYSQL_DSN $REDIS_ADDR $REDIS_DB $REDIS_PASSWORD $TOKEN_SECRET_KEY $QUEUE_DRIVER $KAFKA_ADDRESS $KAFKA_VERSION $KAFKA_USERNAME $KAFKA_PASSWORD $KAFKA_SASL_MECHANISM $IM_ADMIN_API_BASE_URL $IM_ADMIN_API_SERVICE_ID $IM_ADMIN_API_SECRET $IM_INTERNAL_API_BASE_URL $IM_INTERNAL_API_SERVICE_NAME $IM_INTERNAL_API_SECRET $IM_CONFIG_API_SERVICE_NAME $IM_CONFIG_API_SECRET' \
< manifest/config/config.template > manifest/config/config.yaml
echo "queue.driver=${QUEUE_DRIVER:-}"
echo "queue.kafka.address=${KAFKA_ADDRESS:-}"
+echo "imAdminApi.baseUrl=${IM_ADMIN_API_BASE_URL:-}"
exec ./hotgo
diff --git a/server/storage/data/generate/im_abuse_reason_menu.sql b/server/storage/data/generate/im_abuse_reason_menu.sql
index f235603..baa637c 100644
--- a/server/storage/data/generate/im_abuse_reason_menu.sql
+++ b/server/storage/data/generate/im_abuse_reason_menu.sql
@@ -1,16 +1,16 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '举报原因', 'imAbuseReasonIndex', 'abuse-reason/index', '', '2', '', '/imAbuseReason/list', '', '/imAbuseReason/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '82', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex');
SET @imAbuseReasonId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imAbuseReasonId, '原因编辑', 'imAbuseReasonEdit', '', '', '3', '', '/imAbuseReason/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonEdit');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imAbuseReasonId, '原因禁用', 'imAbuseReasonDelete', '', '', '3', '', '/imAbuseReason/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAbuseReasonId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonDelete');
diff --git a/server/storage/data/generate/im_app_release_menu.sql b/server/storage/data/generate/im_app_release_menu.sql
index 20f3635..361d4e5 100644
--- a/server/storage/data/generate/im_app_release_menu.sql
+++ b/server/storage/data/generate/im_app_release_menu.sql
@@ -1,12 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '版本更新', 'imAppReleaseIndex', 'app-release/index', '', '2', '', '/imAppRelease/list', '', '/imAppRelease/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '81', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex');
SET @imAppReleaseId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imAppReleaseId, '版本编辑', 'imAppReleaseEdit', '', '', '3', '', '/imAppRelease/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imAppReleaseId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseEdit');
diff --git a/server/storage/data/generate/im_feedback_config_menu.sql b/server/storage/data/generate/im_feedback_config_menu.sql
index 711a6f7..0746731 100644
--- a/server/storage/data/generate/im_feedback_config_menu.sql
+++ b/server/storage/data/generate/im_feedback_config_menu.sql
@@ -1,12 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '意见反馈配置', 'imFeedbackConfigIndex', 'feedback-config/index', '', '2', '', '/imFeedbackConfig/get', '', '/imFeedbackConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '84', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex');
SET @imFeedbackConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imFeedbackConfigId, '反馈配置更新', 'imFeedbackConfigUpdate', '', '', '3', '', '/imFeedbackConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imFeedbackConfigId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigUpdate');
diff --git a/server/storage/data/generate/im_file_policy_menu.sql b/server/storage/data/generate/im_file_policy_menu.sql
index f1e2591..2503e92 100644
--- a/server/storage/data/generate/im_file_policy_menu.sql
+++ b/server/storage/data/generate/im_file_policy_menu.sql
@@ -20,12 +20,12 @@ CREATE TABLE IF NOT EXISTS `im_file_policy` (
KEY `idx_im_file_policy_tenant_cleanup` (`tenant_id`, `auto_cleanup`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM文件用途策略';
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '文件策略', 'imFilePolicyIndex', 'file-policy/index', '', '2', '', '/imFilePolicy/list', '', '/imFilePolicy/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '73', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFilePolicyIndex');
SET @imFilePolicyId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imFilePolicyIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imFilePolicyId, '策略更新', 'imFilePolicyUpdate', '', '', '3', '', '/imFilePolicy/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imFilePolicyId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imFilePolicyUpdate');
diff --git a/server/storage/data/generate/im_mailbox_config_menu.sql b/server/storage/data/generate/im_mailbox_config_menu.sql
index ecca619..2bd9eed 100644
--- a/server/storage/data/generate/im_mailbox_config_menu.sql
+++ b/server/storage/data/generate/im_mailbox_config_menu.sql
@@ -1,12 +1,12 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '总经理信箱配置', 'imMailboxConfigIndex', 'mailbox-config/index', '', '2', '', '/imMailboxConfig/get', '', '/imMailboxConfig/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '83', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex');
SET @imMailboxConfigId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imMailboxConfigId, '信箱配置更新', 'imMailboxConfigUpdate', '', '', '3', '', '/imMailboxConfig/update', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imMailboxConfigId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigUpdate');
diff --git a/server/storage/data/generate/im_menu_title_repair.sql b/server/storage/data/generate/im_menu_title_repair.sql
new file mode 100644
index 0000000..d2b3f47
--- /dev/null
+++ b/server/storage/data/generate/im_menu_title_repair.sql
@@ -0,0 +1,117 @@
+SET NAMES utf8mb4;
+
+UPDATE `hg_admin_menu` SET `title` = 'IM管理' WHERE `name` = 'imManager';
+UPDATE `hg_admin_menu` SET `title` = '租户管理' WHERE `name` = 'imTenant';
+UPDATE `hg_admin_menu` SET `title` = '租户列表' WHERE `name` = 'imTenantIndex';
+UPDATE `hg_admin_menu` SET `title` = '租户详情' WHERE `name` = 'imTenantView';
+UPDATE `hg_admin_menu` SET `title` = '新增/编辑租户' WHERE `name` = 'imTenantEdit';
+UPDATE `hg_admin_menu` SET `title` = '启用/停用租户' WHERE `name` = 'imTenantStatus';
+UPDATE `hg_admin_menu` SET `title` = '租户用量统计' WHERE `name` = 'imTenantUsage';
+UPDATE `hg_admin_menu` SET `title` = '租户删除检查' WHERE `name` = 'imTenantDeletionCheck';
+UPDATE `hg_admin_menu` SET `title` = '进入删除保护期' WHERE `name` = 'imTenantEnterDeleting';
+UPDATE `hg_admin_menu` SET `title` = '恢复租户' WHERE `name` = 'imTenantRestore';
+
+UPDATE `hg_admin_menu` SET `title` = '用户管理' WHERE `name` = 'imUser';
+UPDATE `hg_admin_menu` SET `title` = '用户列表' WHERE `name` = 'imUserIndex';
+UPDATE `hg_admin_menu` SET `title` = '用户详情' WHERE `name` = 'imUserView';
+UPDATE `hg_admin_menu` SET `title` = '重置密码' WHERE `name` = 'imUserResetPassword';
+UPDATE `hg_admin_menu` SET `title` = '封禁用户' WHERE `name` = 'imUserBan';
+UPDATE `hg_admin_menu` SET `title` = '解封用户' WHERE `name` = 'imUserUnban';
+UPDATE `hg_admin_menu` SET `title` = '冻结用户' WHERE `name` = 'imUserFreeze';
+UPDATE `hg_admin_menu` SET `title` = '解冻用户' WHERE `name` = 'imUserUnfreeze';
+UPDATE `hg_admin_menu` SET `title` = '踢出设备' WHERE `name` = 'imUserDeviceKick';
+UPDATE `hg_admin_menu` SET `title` = '退出所有设备' WHERE `name` = 'imUserLogoutAll';
+UPDATE `hg_admin_menu` SET `title` = '注销账号' WHERE `name` = 'imUserAccountCancel';
+UPDATE `hg_admin_menu` SET `title` = '删除账号' WHERE `name` = 'imUserAccountDelete';
+
+UPDATE `hg_admin_menu` SET `title` = '群组管理' WHERE `name` = 'imGroup';
+UPDATE `hg_admin_menu` SET `title` = '群组列表' WHERE `name` = 'imGroupIndex';
+UPDATE `hg_admin_menu` SET `title` = '群组详情' WHERE `name` = 'imGroupView';
+UPDATE `hg_admin_menu` SET `title` = '群成员列表' WHERE `name` = 'imGroupMemberList';
+UPDATE `hg_admin_menu` SET `title` = '群治理配置' WHERE `name` = 'imGroupGovernance';
+UPDATE `hg_admin_menu` SET `title` = '入群审批列表' WHERE `name` = 'imGroupJoinRequestList';
+UPDATE `hg_admin_menu` SET `title` = '群邀请列表' WHERE `name` = 'imGroupInvitationList';
+UPDATE `hg_admin_menu` SET `title` = '群公告列表' WHERE `name` = 'imGroupAnnouncementList';
+UPDATE `hg_admin_menu` SET `title` = '群禁言列表' WHERE `name` = 'imGroupMuteList';
+UPDATE `hg_admin_menu` SET `title` = '群管理日志' WHERE `name` = 'imGroupAdminLogList';
+UPDATE `hg_admin_menu` SET `title` = '更新群资料' WHERE `name` = 'imGroupProfileUpdate';
+UPDATE `hg_admin_menu` SET `title` = '更新群治理配置' WHERE `name` = 'imGroupGovernanceUpdate';
+UPDATE `hg_admin_menu` SET `title` = '设置群管理员' WHERE `name` = 'imGroupAdminSet';
+UPDATE `hg_admin_menu` SET `title` = '移除群管理员' WHERE `name` = 'imGroupAdminRemove';
+UPDATE `hg_admin_menu` SET `title` = '禁言群成员' WHERE `name` = 'imGroupMemberMute';
+UPDATE `hg_admin_menu` SET `title` = '解除群成员禁言' WHERE `name` = 'imGroupMemberUnmute';
+UPDATE `hg_admin_menu` SET `title` = '移除群成员' WHERE `name` = 'imGroupMemberRemove';
+UPDATE `hg_admin_menu` SET `title` = '转让群主' WHERE `name` = 'imGroupOwnerTransfer';
+
+UPDATE `hg_admin_menu` SET `title` = '注册邀请码' WHERE `name` = 'imRegistrationInviteIndex';
+UPDATE `hg_admin_menu` SET `title` = '创建邀请码' WHERE `name` = 'imInviteCreate';
+UPDATE `hg_admin_menu` SET `title` = '批量创建邀请码' WHERE `name` = 'imInviteBatchCreate';
+UPDATE `hg_admin_menu` SET `title` = '批次详情' WHERE `name` = 'imInviteBatchView';
+UPDATE `hg_admin_menu` SET `title` = '撤销邀请码' WHERE `name` = 'imInviteRevoke';
+UPDATE `hg_admin_menu` SET `title` = '撤销批次邀请码' WHERE `name` = 'imInviteBatchRevoke';
+UPDATE `hg_admin_menu` SET `title` = '导出邀请码' WHERE `name` = 'imInviteExport';
+
+UPDATE `hg_admin_menu` SET `title` = '审计日志' WHERE `name` = 'imAuditLog';
+UPDATE `hg_admin_menu` SET `title` = '审计列表' WHERE `name` = 'imAuditLogIndex';
+UPDATE `hg_admin_menu` SET `title` = '审计详情' WHERE `name` = 'imAuditLogView';
+UPDATE `hg_admin_menu` SET `title` = '导出审计日志' WHERE `name` = 'imAuditLogExport';
+
+UPDATE `hg_admin_menu` SET `title` = '组织架构' WHERE `name` = 'imOrgIndex';
+UPDATE `hg_admin_menu` SET `title` = '部门编辑' WHERE `name` = 'imOrgDepartmentEdit';
+UPDATE `hg_admin_menu` SET `title` = '导出部门' WHERE `name` = 'imOrgDepartmentExport';
+UPDATE `hg_admin_menu` SET `title` = '删除部门' WHERE `name` = 'imOrgDepartmentDelete';
+UPDATE `hg_admin_menu` SET `title` = '成员列表' WHERE `name` = 'imOrgMemberList';
+UPDATE `hg_admin_menu` SET `title` = '成员编辑' WHERE `name` = 'imOrgMemberEdit';
+UPDATE `hg_admin_menu` SET `title` = '导出成员' WHERE `name` = 'imOrgMemberExport';
+UPDATE `hg_admin_menu` SET `title` = '移除成员' WHERE `name` = 'imOrgMemberDelete';
+UPDATE `hg_admin_menu` SET `title` = '组织搜索' WHERE `name` = 'imOrgSearch';
+
+UPDATE `hg_admin_menu` SET `title` = '文件治理' WHERE `name` = 'imFileIndex';
+UPDATE `hg_admin_menu` SET `title` = '文件详情' WHERE `name` = 'imFileView';
+UPDATE `hg_admin_menu` SET `title` = '隔离文件' WHERE `name` = 'imFileQuarantine';
+UPDATE `hg_admin_menu` SET `title` = '删除文件' WHERE `name` = 'imFileDelete';
+UPDATE `hg_admin_menu` SET `title` = '清理孤儿文件' WHERE `name` = 'imFileCleanupOrphan';
+UPDATE `hg_admin_menu` SET `title` = '文件策略' WHERE `name` = 'imFilePolicyIndex';
+UPDATE `hg_admin_menu` SET `title` = '策略更新' WHERE `name` = 'imFilePolicyUpdate';
+
+UPDATE `hg_admin_menu` SET `title` = '通知模板' WHERE `name` = 'imNotificationIndex';
+UPDATE `hg_admin_menu` SET `title` = '模板更新' WHERE `name` = 'imNotificationTemplateUpdate';
+
+UPDATE `hg_admin_menu` SET `title` = '外部服务配置' WHERE `name` = 'imExternalConfigIndex';
+UPDATE `hg_admin_menu` SET `title` = '配置详情' WHERE `name` = 'imExternalConfigView';
+UPDATE `hg_admin_menu` SET `title` = '更新配置' WHERE `name` = 'imExternalConfigUpdate';
+UPDATE `hg_admin_menu` SET `title` = '检查配置' WHERE `name` = 'imExternalConfigCheck';
+
+UPDATE `hg_admin_menu` SET `title` = '工作台配置' WHERE `name` = 'imWorkbenchIndex';
+UPDATE `hg_admin_menu` SET `title` = '模块编辑' WHERE `name` = 'imWorkbenchEdit';
+UPDATE `hg_admin_menu` SET `title` = '模块删除' WHERE `name` = 'imWorkbenchDelete';
+UPDATE `hg_admin_menu` SET `title` = '模块排序' WHERE `name` = 'imWorkbenchReorder';
+
+UPDATE `hg_admin_menu` SET `title` = '版本更新' WHERE `name` = 'imAppReleaseIndex';
+UPDATE `hg_admin_menu` SET `title` = '版本编辑' WHERE `name` = 'imAppReleaseEdit';
+
+UPDATE `hg_admin_menu` SET `title` = '举报原因' WHERE `name` = 'imAbuseReasonIndex';
+UPDATE `hg_admin_menu` SET `title` = '原因编辑' WHERE `name` = 'imAbuseReasonEdit';
+UPDATE `hg_admin_menu` SET `title` = '原因删除' WHERE `name` = 'imAbuseReasonDelete';
+
+UPDATE `hg_admin_menu` SET `title` = '总经理信箱配置' WHERE `name` = 'imMailboxConfigIndex';
+UPDATE `hg_admin_menu` SET `title` = '更新信箱配置' WHERE `name` = 'imMailboxConfigUpdate';
+
+UPDATE `hg_admin_menu` SET `title` = '意见反馈配置' WHERE `name` = 'imFeedbackConfigIndex';
+UPDATE `hg_admin_menu` SET `title` = '更新反馈配置' WHERE `name` = 'imFeedbackConfigUpdate';
+
+UPDATE `hg_admin_menu` SET `title` = '任务看板' WHERE `name` = 'imTaskDashboardIndex';
+
+UPDATE `hg_admin_menu` SET `title` = 'Kafka命令' WHERE `name` = 'imKafkaCommandIndex';
+UPDATE `hg_admin_menu` SET `title` = '命令详情' WHERE `name` = 'imKafkaCommandView';
+UPDATE `hg_admin_menu` SET `title` = '发送缓存失效测试' WHERE `name` = 'imKafkaCommandCacheInvalidate';
+UPDATE `hg_admin_menu` SET `title` = '发送工作台测试事件' WHERE `name` = 'imKafkaCommandWorkbenchEvent';
+
+UPDATE `hg_admin_menu` SET `title` = 'Kafka死信' WHERE `name` = 'imDeadLetterIndex';
+UPDATE `hg_admin_menu` SET `title` = '死信详情' WHERE `name` = 'imDeadLetterView';
+UPDATE `hg_admin_menu` SET `title` = '重放死信' WHERE `name` = 'imDeadLetterReplay';
+UPDATE `hg_admin_menu` SET `title` = '忽略死信' WHERE `name` = 'imDeadLetterIgnore';
+UPDATE `hg_admin_menu` SET `title` = '标记已处理' WHERE `name` = 'imDeadLetterResolve';
+
+UPDATE `hg_admin_menu` SET `title` = 'Kafka审计事件' WHERE `name` = 'imKafkaAuditIndex';
+UPDATE `hg_admin_menu` SET `title` = '审计事件详情' WHERE `name` = 'imKafkaAuditView';
diff --git a/server/storage/data/generate/im_missing_menu_patch.sql b/server/storage/data/generate/im_missing_menu_patch.sql
index 98cceba..d8aee92 100644
--- a/server/storage/data/generate/im_missing_menu_patch.sql
+++ b/server/storage/data/generate/im_missing_menu_patch.sql
@@ -26,18 +26,6 @@ FROM (
WHERE @imRootId IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` m WHERE m.`name` = p.name);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, @imRootId, 'Outbox运维', 'imOutbox', 'outbox', '', 1, '/im/outbox/index', '', '', 'ParentLayout', 1, '', 0, 0, '', 0, 0, 0, 2, CONCAT('tr_', @imRootId, ' '), 65, '', 1, @now, @now
-WHERE @imRootId IS NOT NULL
- AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOutbox');
-
-SELECT @outboxDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutbox' LIMIT 1;
-
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, @outboxDirId, 'Outbox列表', 'imOutboxIndex', 'index', '', 2, '', '/imOutbox/list', '', '/imOutbox/index', 1, 'imOutbox', 0, 0, '', 0, 0, 0, 3, CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' '), 10, '', 1, @now, @now
-WHERE @outboxDirId IS NOT NULL
- AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOutboxIndex');
-
SELECT @imOrgId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex' LIMIT 1;
SELECT @fileId := `id` FROM `hg_admin_menu` WHERE `name` = 'imFileIndex' LIMIT 1;
SELECT @filePolicyId := `id` FROM `hg_admin_menu` WHERE `name` = 'imFilePolicyIndex' LIMIT 1;
@@ -48,7 +36,6 @@ SELECT @appReleaseId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAppReleaseIn
SELECT @abuseReasonId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAbuseReasonIndex' LIMIT 1;
SELECT @mailboxConfigId := `id` FROM `hg_admin_menu` WHERE `name` = 'imMailboxConfigIndex' LIMIT 1;
SELECT @feedbackConfigId := `id` FROM `hg_admin_menu` WHERE `name` = 'imFeedbackConfigIndex' LIMIT 1;
-SELECT @outboxListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutboxIndex' LIMIT 1;
SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' LIMIT 1;
SELECT @auditLogId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' LIMIT 1;
@@ -80,9 +67,6 @@ FROM (
SELECT @abuseReasonId, '原因禁用', 'imAbuseReasonDelete', '/imAbuseReason/delete', 3, CONCAT('tr_', @imRootId, ' tr_', @abuseReasonId, ' '), 20 UNION ALL
SELECT @mailboxConfigId, '信箱配置更新', 'imMailboxConfigUpdate', '/imMailboxConfig/update', 3, CONCAT('tr_', @imRootId, ' tr_', @mailboxConfigId, ' '), 10 UNION ALL
SELECT @feedbackConfigId, '反馈配置更新', 'imFeedbackConfigUpdate', '/imFeedbackConfig/update', 3, CONCAT('tr_', @imRootId, ' tr_', @feedbackConfigId, ' '), 10 UNION ALL
- SELECT @outboxListId, 'Outbox详情', 'imOutboxView', '/imOutbox/view', 4, CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' tr_', @outboxListId, ' '), 10 UNION ALL
- SELECT @outboxListId, '重试Outbox事件', 'imOutboxRetry', '/imOutbox/retry', 4, CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' tr_', @outboxListId, ' '), 20 UNION ALL
- SELECT @outboxListId, '标记Outbox死信', 'imOutboxDead', '/imOutbox/dead', 4, CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' tr_', @outboxListId, ' '), 30 UNION ALL
SELECT @tenantListId, '租户用量与健康', 'imTenantUsage', '/imTenant/usage', 4, CONCAT('tr_', @imRootId, ' tr_', (SELECT `pid` FROM `hg_admin_menu` WHERE `id` = @tenantListId), ' tr_', @tenantListId, ' '), 40 UNION ALL
SELECT @tenantListId, '租户删除前检查', 'imTenantDeletionCheck', '/imTenant/deletionCheck', 4, CONCAT('tr_', @imRootId, ' tr_', (SELECT `pid` FROM `hg_admin_menu` WHERE `id` = @tenantListId), ' tr_', @tenantListId, ' '), 50 UNION ALL
SELECT @tenantListId, '租户进入删除流程', 'imTenantEnterDeleting', '/imTenant/enterDeleting', 4, CONCAT('tr_', @imRootId, ' tr_', (SELECT `pid` FROM `hg_admin_menu` WHERE `id` = @tenantListId), ' tr_', @tenantListId, ' '), 60 UNION ALL
diff --git a/server/storage/data/generate/im_notification_menu.sql b/server/storage/data/generate/im_notification_menu.sql
index 4107d2b..1049aef 100644
--- a/server/storage/data/generate/im_notification_menu.sql
+++ b/server/storage/data/generate/im_notification_menu.sql
@@ -20,12 +20,12 @@ CREATE TABLE IF NOT EXISTS `im_notification_template` (
KEY `idx_im_notification_template_tenant_status` (`tenant_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='IM通知模板配置';
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '通知模板', 'imNotificationIndex', 'notification/index', '', '2', '', '/imNotification/templateList', '', '/imNotification/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '75', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imNotificationIndex');
SET @imNotificationId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imNotificationIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imNotificationId, '模板更新', 'imNotificationTemplateUpdate', '', '', '3', '', '/imNotification/templateUpdate', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imNotificationId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imNotificationTemplateUpdate');
diff --git a/server/storage/data/generate/im_org_menu.sql b/server/storage/data/generate/im_org_menu.sql
index f9fcfa0..e4737d1 100644
--- a/server/storage/data/generate/im_org_menu.sql
+++ b/server/storage/data/generate/im_org_menu.sql
@@ -1,40 +1,40 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '组织架构', 'imOrgIndex', 'org/index', '', '2', '', '/imOrg/departmentList', '', '/imOrg/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '65', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex');
SET @imOrgId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imOrgIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '部门编辑', 'imOrgDepartmentEdit', '', '', '3', '', '/imOrg/departmentEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentEdit');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '部门导出', 'imOrgDepartmentExport', '', '', '3', '', '/imOrg/departmentExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '15', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentExport');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '部门删除', 'imOrgDepartmentDelete', '', '', '3', '', '/imOrg/departmentDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgDepartmentDelete');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '成员列表', 'imOrgMemberList', '', '', '3', '', '/imOrg/memberList', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberList');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '成员编辑', 'imOrgMemberEdit', '', '', '3', '', '/imOrg/memberEdit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '40', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberEdit');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '成员导出', 'imOrgMemberExport', '', '', '3', '', '/imOrg/memberExport', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '45', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberExport');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '成员移除', 'imOrgMemberDelete', '', '', '3', '', '/imOrg/memberDelete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '50', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgMemberDelete');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imOrgId, '组织搜索', 'imOrgSearch', '', '', '3', '', '/imOrg/search', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imOrgId, ' '), '60', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imOrgSearch');
diff --git a/server/storage/data/generate/im_outbox_menu.sql b/server/storage/data/generate/im_outbox_menu.sql
index 90d07c3..b65ada8 100644
--- a/server/storage/data/generate/im_outbox_menu.sql
+++ b/server/storage/data/generate/im_outbox_menu.sql
@@ -1,35 +1,16 @@
+-- Outbox运维菜单已废弃。
+-- 现架构 Kafka 事件直接落库,不再使用旧 `business_outbox` / `group_outbox` 运维入口。
+
SET @now = NOW();
-SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
-
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, '0', 'IM管理', 'imManager', '/im', '', '1', '/im/tenant/index', '', '', 'LAYOUT', '1', '', '1', '0', '', '0', '0', '0', '1', '', '200', '', '1', @now, @now
-WHERE @imRootId IS NULL;
-
-SELECT @imRootId := `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1;
-
-SELECT @outboxDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutbox' LIMIT 1;
-
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, @imRootId, 'Outbox运维', 'imOutbox', 'outbox', '', '1', '/im/outbox/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '65', '', '1', @now, @now
-WHERE @outboxDirId IS NULL;
-
-SELECT @outboxDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutbox' LIMIT 1;
-
-SELECT @outboxListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutboxIndex' LIMIT 1;
-
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, @outboxDirId, 'Outbox列表', 'imOutboxIndex', 'index', '', '2', '', '/imOutbox/list', '', '/imOutbox/index', '1', 'imOutbox', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' '), '10', '', '1', @now, @now
-WHERE @outboxListId IS NULL;
-
-SELECT @outboxListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imOutboxIndex' LIMIT 1;
-
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
-SELECT NULL, @outboxListId, title, name, '', '', '3', '', permissions, '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @outboxDirId, ' tr_', @outboxListId, ' '), sort, '', '1', @now, @now
-FROM (
- SELECT 'Outbox详情' title, 'imOutboxView' name, '/imOutbox/view' permissions, 10 sort UNION ALL
- SELECT '重试Outbox事件', 'imOutboxRetry', '/imOutbox/retry', 20 UNION ALL
- SELECT '标记Outbox死信', 'imOutboxDead', '/imOutbox/dead', 30
-) p
-WHERE @outboxListId IS NOT NULL
- AND NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = p.name);
+UPDATE `hg_admin_menu`
+SET `hidden` = 1,
+ `status` = 2,
+ `updated_at` = @now
+WHERE `name` IN (
+ 'imOutbox',
+ 'imOutboxIndex',
+ 'imOutboxView',
+ 'imOutboxRetry',
+ 'imOutboxDead'
+);
diff --git a/server/storage/data/generate/im_workbench_menu.sql b/server/storage/data/generate/im_workbench_menu.sql
index 0b28785..c08e824 100644
--- a/server/storage/data/generate/im_workbench_menu.sql
+++ b/server/storage/data/generate/im_workbench_menu.sql
@@ -1,20 +1,20 @@
SET @now := NOW();
SET @imRootId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imManager' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imRootId, '工作台配置', 'imWorkbenchIndex', 'workbench/index', '', '2', '', '/imWorkbench/list', '', '/imWorkbench/index', '1', 'imManager', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '80', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex');
SET @imWorkbenchId := (SELECT `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchIndex' LIMIT 1);
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台编辑', 'imWorkbenchEdit', '', '', '3', '', '/imWorkbench/edit', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '10', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchEdit');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台禁用', 'imWorkbenchDelete', '', '', '3', '', '/imWorkbench/delete', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '20', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchDelete');
-INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `component`, `route`, `hidden`, `active_menu`, `is_frame`, `is_cache`, `affix`, `sort`, `status`, `created_by`, `updated_by`, `remark`, `level`, `tree`, `created_at`, `updated_at`)
+INSERT INTO `hg_admin_menu` (`id`, `pid`, `title`, `name`, `path`, `icon`, `type`, `redirect`, `permissions`, `permission_name`, `component`, `always_show`, `active_menu`, `is_root`, `is_frame`, `frame_src`, `keep_alive`, `hidden`, `affix`, `level`, `tree`, `sort`, `remark`, `status`, `created_at`, `updated_at`)
SELECT NULL, @imWorkbenchId, '工作台排序', 'imWorkbenchReorder', '', '', '3', '', '/imWorkbench/reorder', '', '', '1', '', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @imWorkbenchId, ' '), '30', '', '1', @now, @now
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkbenchReorder');
diff --git a/web/src/views/imOrg/index.vue b/web/src/views/imOrg/index.vue
index 0e86b4f..d198500 100644
--- a/web/src/views/imOrg/index.vue
+++ b/web/src/views/imOrg/index.vue
@@ -155,7 +155,11 @@
/>
-
+
diff --git a/web/src/views/imOrg/model.ts b/web/src/views/imOrg/model.ts
index f2904d7..8335ba2 100644
--- a/web/src/views/imOrg/model.ts
+++ b/web/src/views/imOrg/model.ts
@@ -35,7 +35,7 @@ export class MemberState {
public id = 0;
public departmentId = '';
public departmentName = '';
- public userId = 0;
+ public userId = '';
public displayName = '';
public position = '';
public avatarFileId = '';
@@ -177,7 +177,7 @@ export const memberRules = {
type: 'string',
message: '请选择部门',
},
- userId: { required: true, trigger: ['blur', 'input'], type: 'number', message: '请输入用户ID' },
+ userId: { required: true, trigger: ['blur', 'input'], type: 'string', message: '请输入用户ID' },
employmentState: {
required: true,
trigger: ['blur', 'change'],