feat: add tenant-aware IM management console
This commit is contained in:
@@ -0,0 +1,747 @@
|
||||
# IM HotGo 注册短信开关与邀请码管理改造交接文档
|
||||
|
||||
> 文档状态:已按数据库直连方案实现,作为现状与后续 Kafka 演进说明
|
||||
> 对应实施顺序:注册改造计划第 4 项(HotGo 后台)
|
||||
> HotGo 项目:`/Users/youshunlai/go_workspace/im_manager`
|
||||
> IM 服务项目:`/Users/youshunlai/go_workspace/im_server`
|
||||
> 编写日期:2026-08-18
|
||||
|
||||
## 1. 文档用途
|
||||
|
||||
本文供新的开发会话直接接手 HotGo 项目使用。阅读本文后,应能明确:
|
||||
|
||||
1. 为什么要在 HotGo 中增加注册短信开关和邀请码管理。
|
||||
2. 哪些数据属于 HotGo 控制库,哪些数据属于 IM 租户业务库。
|
||||
3. HotGo 后端、前端、菜单权限、统一租户身份、审计和配置需要修改什么。
|
||||
4. HotGo 与 IM 如何通过租户业务库协作,以及后续如何迁移到 Kafka。
|
||||
5. 如何验证实现没有破坏租户隔离,也没有泄露邀请码明文。
|
||||
|
||||
本文指导 HotGo 项目改造。HotGo 负责邀请码生成和管理;IM 仍独占客户端注册、短信验证码和邀请码使用记录事务。
|
||||
|
||||
## 2. 开发前必须阅读
|
||||
|
||||
在修改 `/Users/youshunlai/go_workspace/im_manager` 前,先阅读以下文档:
|
||||
|
||||
1. `/Users/youshunlai/go_workspace/im_server/docs/IM-注册、邀请码与认证数据模型改造需求文档.md`
|
||||
2. `/Users/youshunlai/go_workspace/im_server/docs/IM-前后端对接文档.md` 中 1.4、1.5 注册章节
|
||||
3. `/Users/youshunlai/go_workspace/im_server/docs/IM-后台需求文档.md` 中租户权限、用户管理和审计章节
|
||||
4. `/Users/youshunlai/go_workspace/im_server/docs/IM-独立库多租户改造方案.md`
|
||||
|
||||
规则冲突时按以下优先级处理:
|
||||
|
||||
1. 本文针对 HotGo 的落地边界和接口约定。
|
||||
2. 《IM 注册、邀请码与认证数据模型改造需求文档》。
|
||||
3. 《IM 独立库多租户改造方案》。
|
||||
4. 其他较早的需求和设计文档。
|
||||
|
||||
`IM-服务端多租户改造方案.md` 已废弃,不得作为实现依据。当前多租户方案是“一套 IM 服务、一个 HotGo 控制库、每租户一个独立 IM 业务库”。
|
||||
|
||||
## 3. 需求前因后果
|
||||
|
||||
### 3.1 注册页面最终规则
|
||||
|
||||
客户端注册页面同时展示用户名、手机号、短信验证码、密码和邀请码,但用户名与手机号是二选一关系:
|
||||
|
||||
1. 手机号非空时,以手机号方式为准,忽略用户名输入。
|
||||
2. 手机号方式注册后,`im_users.username` 和 `im_users.phone` 都保存规范化后的 `+86` 手机号。
|
||||
3. 手机号为空时使用普通用户名,用户名必须匹配 `^[a-z0-9]{1,10}$`,此时 `phone=NULL`。
|
||||
4. 普通用户名方式不能自助找回密码,只能由企业管理员重置。
|
||||
5. 邀请码固定为 5 位 ASCII 大小写英文字母,区分大小写、租户内唯一,有效期内可供多人使用。
|
||||
|
||||
### 3.2 为什么需要短信开关
|
||||
|
||||
短信服务在不同部署环境中可能暂未启用,因此需要平台级开关:
|
||||
|
||||
| 场景 | 短信开关开启 | 短信开关关闭 |
|
||||
| --- | --- | --- |
|
||||
| 点击获取验证码 | IM 服务真实调用短信服务商 | IM 服务返回正常挑战和倒计时,但不调用服务商 |
|
||||
| 手机号注册 | 必须验证短信;邀请码不能绕过 | 必须提供有效邀请码,可无验证码注册 |
|
||||
| 用户名注册 | 不发送短信,必须提供有效邀请码 | 不发送短信,必须提供有效邀请码 |
|
||||
| 客户端是否知道开关 | 不直接暴露 | 不直接暴露 |
|
||||
|
||||
HotGo 只负责管理开关值。是否调用短信服务商、是否允许注册,全部由 IM 服务端判定。
|
||||
|
||||
### 3.3 为什么当前改为 HotGo 直接访问租户库
|
||||
|
||||
邀请码属于 IM 租户业务数据:
|
||||
|
||||
1. 每个租户有独立业务库。
|
||||
2. 邀请码批次和主表位于 `hotgo` 控制库,使用明细位于对应租户业务库。
|
||||
3. HotGo 邀请码管理直接访问控制库;密码重置等租户业务功能仍通过 `im_tenant.id` 和服务端保存的 DSN 选择租户库。
|
||||
4. 当前部署中 HotGo 到 IM 的内部 HTTP 地址不可达,继续维护 HMAC 管理 API 会增加一套没有必要的调用链。
|
||||
|
||||
因此当前采用“控制库主数据 + 租户库使用事实”的混合边界:HotGo 负责邀请码生成、查询、导出和撤销;IM 从控制库校验邀请码,并在租户库记录使用关系和 outbox 事件。两端各自只写自己负责的数据。
|
||||
|
||||
密码重置等需要 IM 立即感知的操作写入 `im_business_events_outbox`。当前 IM 轮询消费,后续改为 Kafka 时保留事件契约,只替换传输层。
|
||||
|
||||
## 4. 系统边界
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["HotGo 管理后台浏览器"] -->|HotGo 登录 Token| B["HotGo 后端"]
|
||||
B -->|读写全局开关| C[("hotgo.hg_sys_config")]
|
||||
B -->|读取管理员 tenant_id| D[("hotgo.im_tenant")]
|
||||
B -->|管理邀请码| G[("hotgo.im_registration_invite_batches")]
|
||||
B -->|管理邀请码| H[("hotgo.im_registration_invite_codes")]
|
||||
B -->|其他租户业务按服务端 DSN| F[("租户 IM 业务库")]
|
||||
F --> I["im_registration_invite_uses"]
|
||||
J["IM 注册与发码接口"] -->|短时缓存读取| C
|
||||
J -->|按 tenant_id 校验| H
|
||||
J -->|记录使用关系与事件| F
|
||||
B -->|写 security.password_reset| K["im_business_events_outbox"]
|
||||
F --> K
|
||||
K -->|当前轮询,后续 Kafka| J
|
||||
```
|
||||
|
||||
### 4.1 HotGo 负责
|
||||
|
||||
1. 在 `hg_sys_config` 保存全局注册短信开关。
|
||||
2. 只允许平台超级管理员修改开关。
|
||||
3. 为邀请码提供管理页面、表单、权限和审计入口。
|
||||
4. 从当前登录管理员身份解析可信的 IM `tenant_id`。
|
||||
5. 通过 `imtenant.DB()` 直连所选租户业务库,不接受浏览器传入 DSN。
|
||||
6. 对邀请码明文查询、查看和导出执行独立权限校验。
|
||||
|
||||
### 4.2 IM 服务负责
|
||||
|
||||
1. 读取并缓存短信开关。
|
||||
2. 实际发送或抑制短信。
|
||||
3. 注册时大小写敏感地校验邀请码,并记录邀请码与新用户的使用关系。
|
||||
4. 邀请码使用后不改变 active 状态。
|
||||
5. 消费 `security.password_reset` 事件并清退在线会话。
|
||||
6. 登录时以 `auth_version` 拒绝密码重置前签发的旧 Token。
|
||||
7. 消费 `registration.invite_used`,将租户库使用统计幂等投影到控制库。
|
||||
|
||||
### 4.3 明确禁止
|
||||
|
||||
1. 浏览器直接连接租户数据库。
|
||||
2. 浏览器通过 Header、Query 或 Body 自报可信 `tenant_id`。
|
||||
3. HotGo 后端根据浏览器传入的 `db_name` 或 `db_dsn` 连接数据库。
|
||||
4. HotGo 修改 `im_registration_invite_uses`;该表只允许 IM 注册事务写入。
|
||||
5. HotGo 绕过 outbox 直接修改在线会话状态。
|
||||
6. 在普通访问日志、错误日志、埋点或 URL 中记录完整邀请码。
|
||||
|
||||
## 5. 当前 HotGo 项目现状
|
||||
|
||||
目标仓库 `/Users/youshunlai/go_workspace/im_manager` 基于 GoFrame 2、Vue 3 和 Naive UI。
|
||||
|
||||
已确认的现状:
|
||||
|
||||
1. 通用配置后端位于 `server/api/admin/config`、`server/internal/controller/admin/sys/config.go`、`server/internal/logic/sys/config.go`。
|
||||
2. 通用配置前端位于 `web/src/views/system/config` 和 `web/src/api/sys/config.ts`。
|
||||
3. `hg_sys_config` 支持 `group/key/type/value/default_value`,前端不能通过通用接口新增不存在的配置项。
|
||||
4. `sys_switch` 字典实际定义为“开启=`1`、关闭=`2`”。
|
||||
5. HotGo 管理接口权限按“路由路径 + HTTP Method”使用 Casbin 校验,前端按钮权限也使用路由路径,例如 `hasPermission(['/notice/edit'])`。
|
||||
6. `hotgo.im_tenant` 是全系统唯一租户注册表,由 HotGo 维护,不存在另一套需要映射的“IM 租户”。
|
||||
7. 后台管理员通过 `hg_admin_member.tenant_id` 直接引用 `im_tenant.id`,登录身份必须携带该字段;不再使用独立租户绑定表。
|
||||
8. `hg_sys_log` 会保存 GET Query 和 POST Body;现有逻辑只自动掩码名称包含 `password` 的 POST 字段,邀请码查询参数会泄漏,必须补充脱敏。
|
||||
|
||||
## 6. 建议开发顺序
|
||||
|
||||
### 阶段 A:先完成 HotGo 基础边界
|
||||
|
||||
1. 增加全局短信开关配置种子和专用读写接口。
|
||||
2. 增加 `hg_admin_member.tenant_id` 外键与登录身份解析,统一使用 `im_tenant.id`。
|
||||
3. 增加租户库直连、Snowflake ID 和数据库事件写入能力。
|
||||
4. 增加邀请码相关菜单、路由权限和后台代理接口。
|
||||
5. 增加邀请码管理页面。
|
||||
6. 增加敏感字段日志脱敏和操作审计。
|
||||
|
||||
### 阶段 B:与 IM 服务联调
|
||||
|
||||
1. 核对 IM 租户库迁移已执行且表结构一致。
|
||||
2. 核对服务凭证、租户上下文和操作人上下文可被 IM 验证。
|
||||
3. 联调单个生成、批量生成、分页查询、撤销、详情和导出。
|
||||
4. 联调短信开关在 IM 服务缓存时间内生效。
|
||||
|
||||
IM 管理接口未完成时,可以通过接口 Client 抽象和测试 Stub 开发页面,但不得用 HotGo 直写租户数据库作为临时方案。
|
||||
|
||||
## 7. 全局注册短信开关
|
||||
|
||||
### 7.1 数据定义
|
||||
|
||||
在 HotGo 控制库 `hg_sys_config` 中增加且只保留一条配置:
|
||||
|
||||
```text
|
||||
group = im
|
||||
name = 注册短信发送
|
||||
type = bool
|
||||
key = register_sms_enabled
|
||||
value = 1
|
||||
default_value = 1
|
||||
status = 1
|
||||
is_default = 1
|
||||
```
|
||||
|
||||
建议的 MySQL 幂等种子:
|
||||
|
||||
```sql
|
||||
INSERT INTO hg_sys_config
|
||||
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
|
||||
SELECT
|
||||
'im', '注册短信发送', 'bool', 'register_sms_enabled', '1', '1', 10,
|
||||
'控制 IM 注册验证码是否真实发送', 1, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM hg_sys_config
|
||||
WHERE `group` = 'im' AND `key` = 'register_sms_enabled'
|
||||
);
|
||||
```
|
||||
|
||||
同时为 `(group, key)` 增加唯一约束。加索引前先检查并清理重复配置,不能让 IM 服务随机读到多条值。
|
||||
|
||||
```sql
|
||||
ALTER TABLE hg_sys_config
|
||||
ADD UNIQUE KEY uk_hg_sys_config_group_key (`group`, `key`);
|
||||
```
|
||||
|
||||
新装数据库种子、实际部署迁移及项目保留的 MySQL/PostgreSQL/SQLite 初始化脚本应按项目发布策略同步,不能只改一份示例 SQL。
|
||||
|
||||
### 7.2 `sys_switch` 映射陷阱
|
||||
|
||||
`sys_switch` 字典值与真实配置值不相同:
|
||||
|
||||
| UI 字典值 | UI 含义 | `hg_sys_config.value` |
|
||||
| --- | --- | --- |
|
||||
| `1` | 开启 | `1` |
|
||||
| `2` | 关闭 | `0` |
|
||||
|
||||
实现要求:
|
||||
|
||||
1. 页面加载时将后端 bool 值映射为字典值:`true -> 1`,`false -> 2`。
|
||||
2. 页面提交时反向映射:`1 -> true`,`2 -> false`。
|
||||
3. 不得把字典值 `2` 原样写入 bool 配置;GoFrame 的 bool 转换可能把非零值当成 true。
|
||||
4. 数据库只允许保存可解释为 `1/0` 的值。
|
||||
|
||||
### 7.3 后端接口
|
||||
|
||||
不要直接让页面复用通用 `/config/update`。新增专用接口,以便强制执行平台管理员校验:
|
||||
|
||||
| 方法 | HotGo 路径 | 权限 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/imRegistration/config/get` | 平台超级管理员 |
|
||||
| POST | `/imRegistration/config/update` | 平台超级管理员 |
|
||||
|
||||
读取响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"registerSmsEnabled": true,
|
||||
"updatedAt": "2026-08-18T16:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
更新请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"registerSmsEnabled": false
|
||||
}
|
||||
```
|
||||
|
||||
后端必须在业务逻辑层再次调用 `service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx))`,不能只依赖菜单是否可见。更新时只允许修改固定的 `group=im/key=register_sms_enabled`,不接受前端传 group 或 key。
|
||||
|
||||
### 7.4 页面位置
|
||||
|
||||
建议在 `web/src/views/system/config/system.vue` 增加“IM 注册”设置项,并新增:
|
||||
|
||||
```text
|
||||
web/src/views/system/config/ImRegistrationSetting.vue
|
||||
```
|
||||
|
||||
页面只展示一个“注册短信发送”开关和保存按钮。选项通过 `dict.loadOptions(['sys_switch'])` 加载,并按 7.2 做显式映射。
|
||||
|
||||
### 7.5 IM 服务读取约定
|
||||
|
||||
HotGo 不需要额外暴露公开配置接口。IM 服务直接从控制库读取:
|
||||
|
||||
```sql
|
||||
SELECT value
|
||||
FROM hg_sys_config
|
||||
WHERE `group` = 'im'
|
||||
AND `key` = 'register_sms_enabled'
|
||||
AND status = 1
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
约定:
|
||||
|
||||
1. 种子默认值为开启。
|
||||
2. IM 服务缓存建议为 30 秒,缓存时长可配置。
|
||||
3. 临时读取失败时优先使用最近一次成功值。
|
||||
4. 进程启动后无缓存且配置缺失、停用或非法时,按“开启”处理,防止邀请码绕过短信验证,并记录告警。
|
||||
5. HotGo 更新后不承诺立即生效,验收标准为 30 秒内生效。
|
||||
|
||||
## 8. 统一租户身份
|
||||
|
||||
### 8.1 唯一租户主键
|
||||
|
||||
`hotgo.im_tenant.id` 是 HotGo 和 IM 服务共同使用的唯一 `tenant_id`。HotGo 负责维护租户注册信息和业务库路由,IM 服务根据该 ID 选择租户业务库。不得再创建 `im_admin_tenant_binding` 或其他租户映射表。
|
||||
|
||||
### 8.2 管理员租户归属
|
||||
|
||||
后台管理员记录直接引用统一租户:
|
||||
|
||||
```sql
|
||||
ALTER TABLE hg_admin_member
|
||||
ADD COLUMN tenant_id BIGINT NULL COMMENT '统一租户ID,引用 im_tenant.id',
|
||||
ADD INDEX idx_hg_admin_member_tenant (tenant_id),
|
||||
ADD CONSTRAINT fk_hg_admin_member_im_tenant
|
||||
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id);
|
||||
```
|
||||
|
||||
平台账号的 `tenant_id` 可以为空。租户管理员及其创建的下级后台账号必须继承同一个 `im_tenant.id`。只有平台超级管理员可以为后台账号选择或修改租户,租户域管理员提交的 `tenantId` 必须被后端忽略并以当前登录身份为准。
|
||||
|
||||
### 8.3 解析规则
|
||||
|
||||
租户管理员请求邀请码接口时:
|
||||
|
||||
1. 从 HotGo 登录上下文取得 `member_id`、`tenant_id` 和部门类型。
|
||||
2. 仅允许合法的租户级管理员角色进入;普通用户和商户角色不得管理邀请码。
|
||||
3. `tenant_id` 必须大于 0,并且对应的 `im_tenant` 必须存在且为 active。
|
||||
4. 将该 `tenant_id` 写入 IM 服务调用上下文。
|
||||
5. 忽略并拒绝浏览器提交的 `tenantId`。
|
||||
|
||||
平台超级管理员可以跨租户查看邀请码;列表和导出未选择租户时查询全部。创建、批次查看、撤销及其他写操作必须先选择一个 `im_tenant`。此选择只对 `VerifySuperId` 通过的管理员开放,HotGo 后端仍要检查目标租户存在且 active。
|
||||
|
||||
后台用户管理应提供仅平台超级管理员可见的租户选择控件。租户管理员未关联 `im_tenant.id` 时返回明确错误“当前管理员未关联租户”,不能回退到默认租户。
|
||||
|
||||
## 9. 租户库直连与事件通道
|
||||
|
||||
### 9.1 数据库选择
|
||||
|
||||
HotGo 从可信登录上下文取得 `im_tenant.id`。邀请码批次和邀请码主表直接通过 `imtenant.RegistryDB()` 访问 HotGo 控制库;只有密码重置等租户业务操作才通过 `imtenant.DB()` 打开租户业务库。浏览器传入的 `tenantId` 只允许平台超级管理员选择,租户管理员始终使用登录关系中的租户 ID;任何请求都不能提交或覆盖 DSN。
|
||||
|
||||
若配置了 `database.imRegistry`,租户注册表从该连接读取;未配置时回退到 HotGo 默认库。业务操作始终使用按租户动态建立的连接组。
|
||||
|
||||
### 9.2 Snowflake 配置
|
||||
|
||||
HotGo 直接新增 IM 表记录时使用与 IM 相同位布局的 Snowflake ID:
|
||||
|
||||
```yaml
|
||||
imDirectDb:
|
||||
snowflakeWorkerId: 1023
|
||||
```
|
||||
|
||||
也可由 `IM_DIRECT_DB_SNOWFLAKE_WORKER_ID` 覆盖。每个 HotGo 写库实例必须使用唯一 worker ID,并且不能与 IM 节点重叠。当前单实例默认预留 `1023`,多实例部署前必须分配 worker ID。
|
||||
|
||||
### 9.3 写入职责
|
||||
|
||||
HotGo 可以写:
|
||||
|
||||
1. HotGo 控制库中的 `im_registration_invite_batches`、`im_registration_invite_codes`。
|
||||
2. 管理员重置密码时的 `im_auth.secret` 和 `im_user_security`。
|
||||
3. 管理操作产生的 `im_business_events_outbox`。
|
||||
|
||||
IM 独占写入:
|
||||
|
||||
1. 客户端注册事务及用户创建。
|
||||
2. 租户业务库中的 `im_registration_invite_uses` 以及同事务的 `registration.invite_used` outbox 事件。
|
||||
3. 客户端登录、改密、短信找回等认证事务。
|
||||
|
||||
### 9.4 Kafka 演进
|
||||
|
||||
管理员重置密码在数据库事务内写入:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "security.password_reset",
|
||||
"payload": {
|
||||
"user_id": 198000000000000001,
|
||||
"auth_version": 2,
|
||||
"operator_id": 10001,
|
||||
"request_id": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当前 IM 在注册写入 outbox 后立即触发一次消费,并每秒按在线会话涉及的租户轮询 pending 事件。`security.password_reset` 消费后清退该用户会话;`registration.invite_used` 消费时从租户使用明细重新聚合 `COUNT/MAX(used_at)`,覆盖 HotGo 控制库的 `usage_count/last_used_at`,成功后标记 delivered。离线用户不依赖密码重置轮询,后续登录时由 `auth_version` 直接拒绝旧 Token。
|
||||
|
||||
邀请码使用事件契约:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "registration.invite_used",
|
||||
"payload": {
|
||||
"tenant_id": 1,
|
||||
"invite_code_id": 198000000000000001,
|
||||
"user_id": 198000000000000002,
|
||||
"request_id": "acc-request-id",
|
||||
"used_at": "2026-08-19T10:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
迁移 Kafka 时,生产者继续在业务事务内写 outbox,由独立投递器发布同一事件;消费者保持事件类型与 payload 不变。Kafka 投递确认后再标记 outbox delivered,避免数据库提交成功但消息丢失。
|
||||
|
||||
## 10. 邀请码接口契约
|
||||
|
||||
### 10.1 数据表契约
|
||||
|
||||
HotGo 在控制库直接维护带 `tenant_id` 的 `im_registration_invite_batches` 和 `im_registration_invite_codes`。邀请码状态仅为 `active/expired/revoked`,使用后仍为 active;`im_registration_invite_uses` 只由 IM 注册事务写入租户库。控制库中的 `usage_count/last_used_at` 由 outbox 消费器幂等投影,HotGo 列表不跨租户库做联表聚合。
|
||||
|
||||
平台超级管理员在列表和导出接口中可不传 `tenantId` 以查询全部租户;创建、批次查看、撤销等写操作必须明确选择一个 active 租户。租户管理员只能查询和操作登录关系绑定的 `im_tenant.id`。
|
||||
|
||||
邀请码明文不得放入 URL。查询完整邀请码时必须使用 HotGo POST Body,普通请求日志需对 `inviteCode` 脱敏。
|
||||
|
||||
### 10.2 HotGo 对浏览器接口
|
||||
|
||||
沿用 HotGo 现有命名风格,建议增加:
|
||||
|
||||
| 方法 | HotGo 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/imInvite/list` | 分页查询,过滤条件放在 JSON Body |
|
||||
| POST | `/imInvite/create` | 单个生成 |
|
||||
| POST | `/imInvite/batchCreate` | 批量生成 |
|
||||
| GET | `/imInvite/batchView` | 批次详情 |
|
||||
| POST | `/imInvite/revoke` | 单个撤销 |
|
||||
| POST | `/imInvite/batchRevoke` | 批量撤销 |
|
||||
| POST | `/imInvite/export` | 明文导出,过滤条件放在 JSON Body |
|
||||
|
||||
HotGo controller 只做 DTO 转换、调用 service 和包装响应。租户解析、平台管理员判断、权限复核、审计和租户库访问放在 logic/service 层。
|
||||
|
||||
### 10.3 单个生成
|
||||
|
||||
HotGo 请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"expiresAt": "2026-09-01T00:00:00+08:00",
|
||||
"remark": "新员工入职",
|
||||
"idempotencyKey": "7be3de87-994f-46c6-86bd-844ffb54f83f"
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. `expiresAt` 必填,必须晚于服务端当前时间。
|
||||
2. `remark` 最长 255 字符。
|
||||
3. 推荐单个生成也传幂等键,避免提交成功但响应超时时重复创建。
|
||||
4. 邀请码由 HotGo 使用 `crypto/rand` 生成并依靠租户库唯一索引兜底,HotGo 不接受用户自定义 code。
|
||||
|
||||
响应数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 198000000000000001,
|
||||
"code": "AbCdE",
|
||||
"status": "active",
|
||||
"expiresAt": "2026-09-01T00:00:00+08:00",
|
||||
"remark": "新员工入职",
|
||||
"createdBy": 10001,
|
||||
"createdAt": "2026-08-18T16:10:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.4 批量生成
|
||||
|
||||
HotGo 请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"quantity": 100,
|
||||
"expiresAt": "2026-09-01T00:00:00+08:00",
|
||||
"remark": "2026年8月新员工入职",
|
||||
"idempotencyKey": "4dc7f13e-448c-4b49-b056-57a2fde18f95"
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
1. `quantity` 为 1 到 1000,由 HotGo 校验。
|
||||
2. 同一个提交动作必须复用同一个幂等键,直到得到明确成功响应。
|
||||
3. 前端可在打开弹窗时生成 UUID,失败重试时复用;成功或关闭弹窗后废弃。
|
||||
4. HotGo 在批次表保存幂等键;相同参数重试返回原批次,不重复生成。
|
||||
|
||||
响应数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"batchId": 198000000000000100,
|
||||
"quantity": 100,
|
||||
"activeCount": 100,
|
||||
"usageCount": 0,
|
||||
"revokedCount": 0,
|
||||
"expiredCount": 0,
|
||||
"expiresAt": "2026-09-01T00:00:00+08:00",
|
||||
"createdAt": "2026-08-18T16:15:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.5 分页查询
|
||||
|
||||
列表请求使用 POST JSON Body,支持字段:
|
||||
|
||||
```text
|
||||
page, perPage, inviteCode, status, batchId, createdBy,
|
||||
createdAtStart, createdAtEnd, expiresAtStart, expiresAtEnd
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
1. `inviteCode` 仅支持完整 5 位精确查询,不做模糊查询,不转换大小写。
|
||||
2. `status` 为 `active/expired/revoked`,邀请码使用后仍保持 `active`。
|
||||
3. HotGo 查询前将 `expires_at <= now` 的 active 记录更新为 expired,不依赖定时任务。
|
||||
4. 默认按 `created_at DESC, id DESC` 排序。
|
||||
5. `perPage` 设置合理上限,建议不超过 100。
|
||||
|
||||
列表字段至少包括:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 198000000000000001,
|
||||
"batchId": 198000000000000100,
|
||||
"code": "AbCdE",
|
||||
"status": "active",
|
||||
"expiresAt": "2026-09-01T00:00:00+08:00",
|
||||
"usageCount": 12,
|
||||
"lastUsedAt": "2026-08-20T09:30:00+08:00",
|
||||
"createdBy": 10001,
|
||||
"remark": "新员工入职",
|
||||
"createdAt": "2026-08-18T16:10:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.6 撤销
|
||||
|
||||
1. 只有 `active` 且未过期的邀请码可撤销;已有使用记录不影响撤销。
|
||||
2. 已过期、已撤销的邀请码不能恢复为 active。
|
||||
3. 批量撤销只撤销批次内仍可撤销的码,并返回各状态统计。
|
||||
4. 页面操作前显示确认框,确认内容不必重复显示完整邀请码。
|
||||
5. 最终状态由租户库条件更新结果决定,不以前端状态为准。
|
||||
|
||||
### 10.7 导出
|
||||
|
||||
1. 导出必须具备独立 `/imInvite/export` 权限。
|
||||
2. 导出过滤条件与列表一致。
|
||||
3. HotGo 后端分页读取租户库并流式写出,不把整份文件加载到内存。
|
||||
4. 建议输出带 UTF-8 BOM 的 CSV,字段至少包含邀请码、状态、批次、到期时间、累计使用次数、最后使用时间、创建人、创建时间和备注。
|
||||
5. 文件名建议为 `registration-invite-codes-YYYYMMDD-HHmmss.csv`。
|
||||
6. 每次导出必须写审计,但审计详情不得包含邀请码明文列表。
|
||||
|
||||
## 11. 页面和交互
|
||||
|
||||
建议页面路径:
|
||||
|
||||
```text
|
||||
web/src/views/im/registrationInvite/index.vue
|
||||
web/src/views/im/registrationInvite/model.ts
|
||||
web/src/views/im/registrationInvite/CreateModal.vue
|
||||
web/src/views/im/registrationInvite/BatchCreateModal.vue
|
||||
web/src/views/im/registrationInvite/BatchViewModal.vue
|
||||
web/src/api/im/registrationInvite.ts
|
||||
```
|
||||
|
||||
页面放在“IM 管理 / 注册邀请码”菜单下。使用现有 `BasicForm`、`BasicTable`、`TableAction`、`usePermission` 和 Naive UI 组件。
|
||||
|
||||
页面必须具备:
|
||||
|
||||
1. 完整邀请码、状态、批次 ID、创建人、创建时间和到期时间筛选。
|
||||
2. 单个生成按钮。
|
||||
3. 批量生成按钮。
|
||||
4. 导出按钮。
|
||||
5. 列表展示完整邀请码、状态、到期时间、批次、使用情况、创建信息和备注。
|
||||
6. active 行提供撤销操作,其他状态不显示撤销操作。
|
||||
7. 批次详情展示总数、active、used、expired、revoked 统计。
|
||||
8. 所有日期时间明确按本地时区展示,提交时发送带时区的 RFC 3339 时间。
|
||||
9. 加载、空数据、接口错误、重复提交和无权限状态处理完整。
|
||||
|
||||
邀请码区分大小写,任何输入组件、复制操作和查询参数都不得自动转为大写或小写。
|
||||
|
||||
## 12. 菜单与权限
|
||||
|
||||
HotGo 的权限标识使用接口路径。建议创建以下按钮权限:
|
||||
|
||||
```text
|
||||
/imRegistration/config/get
|
||||
/imRegistration/config/update
|
||||
/imInvite/list
|
||||
/imInvite/create
|
||||
/imInvite/batchCreate
|
||||
/imInvite/batchView
|
||||
/imInvite/revoke
|
||||
/imInvite/batchRevoke
|
||||
/imInvite/export
|
||||
```
|
||||
|
||||
权限分配:
|
||||
|
||||
| 能力 | 平台超级管理员 | 租户管理员 | 其他角色 |
|
||||
| --- | --- | --- | --- |
|
||||
| 查看/修改短信开关 | 允许 | 禁止 | 禁止 |
|
||||
| 选择任意 IM 租户管理邀请码 | 允许 | 禁止 | 禁止 |
|
||||
| 管理当前绑定租户邀请码 | 允许 | 按权限允许 | 禁止 |
|
||||
| 查看邀请码明文 | 允许 | 需独立权限 | 禁止 |
|
||||
| 导出邀请码明文 | 允许 | 需独立权限 | 禁止 |
|
||||
|
||||
服务端 Casbin、业务逻辑层租户范围校验、前端按钮隐藏三层都要实现。前端隐藏按钮不是安全边界。
|
||||
|
||||
## 13. 日志脱敏与审计
|
||||
|
||||
### 13.1 必须修复现有访问日志泄露
|
||||
|
||||
`server/internal/logic/sys/log.go` 当前会保存 Query 和 Body,且只掩码 password。至少增加以下字段名的大小写不敏感脱敏:
|
||||
|
||||
```text
|
||||
inviteCode
|
||||
invite_code
|
||||
serviceSecret
|
||||
service_secret
|
||||
authorization
|
||||
```
|
||||
|
||||
建议抽成统一的递归敏感字段过滤器,同时处理 Query、JSON Body、Form 和 Header。Header 中已有简化逻辑也必须确认不会保存内部服务凭证。
|
||||
|
||||
对 `/imInvite/list` 的明文精确查询,邀请码只能放在 POST Body,`hg_sys_log.post_data` 中只能出现 `******`。`hg_sys_log.get_data`、Nginx URL 和其他访问日志中都不能出现真实邀请码。HTTP Client 的 debug 日志同样不得打印完整 URL、Query、请求体或响应体。
|
||||
|
||||
### 13.2 业务审计
|
||||
|
||||
仅靠普通访问日志不够。建议在 HotGo 控制库增加独立审计表,或者接入项目已有的等价不可变审计设施。最低字段为:
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_admin_operation_audit (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
tenant_id BIGINT NULL,
|
||||
operator_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL,
|
||||
target_id BIGINT NULL,
|
||||
result VARCHAR(16) NOT NULL,
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
detail_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_im_admin_audit_tenant_time (tenant_id, created_at),
|
||||
KEY idx_im_admin_audit_operator_time (operator_id, created_at),
|
||||
UNIQUE KEY uk_im_admin_audit_request_action (request_id, action)
|
||||
);
|
||||
```
|
||||
|
||||
必须审计:
|
||||
|
||||
1. 短信开关修改,记录旧值和新值。
|
||||
2. 单个生成和批量生成,记录 code ID 或 batch ID、数量和到期时间,但不记录 code 明文。
|
||||
3. 邀请码明文查询和详情查看,记录查询行为,但不记录搜索明文。
|
||||
4. 单个撤销和批量撤销。
|
||||
5. 明文导出,记录过滤条件摘要和导出数量,不记录明文内容。
|
||||
6. 后台管理员租户归属变更。
|
||||
|
||||
IM 服务也应写自己的业务审计。HotGo 审计记录后台操作者,IM 审计记录实际租户库操作,两者通过 `request_id` 关联。
|
||||
|
||||
## 14. 错误处理
|
||||
|
||||
HotGo 应将数据库错误稳定映射为后台可理解的错误,不向浏览器返回 DSN、SQL、堆栈或内部配置。
|
||||
|
||||
至少处理:
|
||||
|
||||
| 场景 | HotGo 行为 |
|
||||
| --- | --- |
|
||||
| 当前管理员未关联租户 | 返回明确业务错误,不使用默认租户 |
|
||||
| 目标租户停用 | 拒绝操作 |
|
||||
| 无查看/导出权限 | 403 |
|
||||
| 邀请码已过期或已撤销 | 显示不可撤销,不本地修改状态 |
|
||||
| 幂等键重复且请求相同 | 返回原结果 |
|
||||
| 幂等键重复但参数不同 | 返回幂等冲突 |
|
||||
| 租户库连接失败 | 返回“租户业务库暂不可用”,保留 Request ID |
|
||||
| 数据库写入失败 | 事务回滚,返回统一后台错误并记录脱敏日志 |
|
||||
| 导出数据过大 | 使用服务端限制或异步导出,不耗尽内存 |
|
||||
|
||||
## 15. 建议代码落点
|
||||
|
||||
遵循现有 HotGo 分层,建议新增或修改:
|
||||
|
||||
```text
|
||||
server/api/admin/imregistration/
|
||||
server/api/admin/iminvite/
|
||||
server/internal/controller/admin/admin/im_registration.go
|
||||
server/internal/controller/admin/admin/im_invite.go
|
||||
server/internal/model/input/adminin/im_registration.go
|
||||
server/internal/model/input/adminin/im_invite.go
|
||||
server/internal/service/im.go
|
||||
server/internal/logic/admin/im_registration.go
|
||||
server/internal/logic/admin/im_invite.go
|
||||
server/internal/library/imtenant/db.go
|
||||
server/internal/library/imtenant/snowflake.go
|
||||
server/internal/router/admin.go
|
||||
server/internal/logic/sys/log.go
|
||||
web/src/api/im/registrationInvite.ts
|
||||
web/src/views/im/registrationInvite/
|
||||
web/src/views/system/config/ImRegistrationSetting.vue
|
||||
web/src/views/system/config/system.vue
|
||||
```
|
||||
|
||||
如果新增 `im_admin_operation_audit`,按 HotGo 现有 GoFrame DAO 生成流程生成 `dao/do/entity`,不要手工修改标有 “Code generated” 的文件。禁止新增 `im_admin_tenant_binding`。
|
||||
|
||||
同时提交:
|
||||
|
||||
1. 生产数据库幂等迁移。
|
||||
2. 新装数据库初始化数据。
|
||||
3. 菜单和按钮权限种子。
|
||||
4. 默认角色授权或明确的部署授权步骤。
|
||||
5. Snowflake worker 配置示例,但不提交真实 DSN。
|
||||
|
||||
## 16. 测试清单
|
||||
|
||||
### 16.1 短信开关
|
||||
|
||||
1. 无配置时 IM 按开启处理并告警。
|
||||
2. 平台超级管理员可查看和修改。
|
||||
3. 租户管理员即使手工调用接口也不能修改。
|
||||
4. UI 选择“关闭”后数据库保存 `0`,不是 `2`。
|
||||
5. UI 重新加载时 `0` 正确显示为“关闭”。
|
||||
6. IM 服务在 30 秒内读取到新值。
|
||||
7. 开关审计包含操作者、旧值、新值和 Request ID。
|
||||
|
||||
### 16.2 租户隔离
|
||||
|
||||
1. 租户 A 管理员只能看到租户 A 邀请码。
|
||||
2. 修改 Query、Body 或 Header 中的 tenant ID 不能访问租户 B。
|
||||
3. 未绑定管理员不能回退到 default 租户。
|
||||
4. 停用租户不能生成、查询、撤销或导出。
|
||||
5. 平台超级管理员选择租户后只能操作所选租户,切换时列表立即清空并重新请求。
|
||||
|
||||
### 16.3 邀请码
|
||||
|
||||
1. 单个生成返回恰好 5 位 `[A-Za-z]`。
|
||||
2. 批量生成数量正确,最大值限制生效。
|
||||
3. `AbCdE` 和 `abcde` 可作为不同邀请码精确查询。
|
||||
4. 重复提交相同幂等键不产生第二个批次。
|
||||
5. active 可撤销,expired/revoked 不可撤销。
|
||||
6. 同一 active 邀请码可供多个不同用户注册,累计使用次数正确增长且状态保持 active。
|
||||
7. 列表、详情和导出权限相互独立。
|
||||
8. 导出文件数量、过滤条件和明文字段正确。
|
||||
|
||||
### 16.4 安全和日志
|
||||
|
||||
1. `hg_sys_log.get_data/post_data/header_data` 中找不到真实邀请码和服务 secret。
|
||||
2. 应用日志中找不到邀请码明文、内部 Token 和 Authorization。
|
||||
3. 每个敏感操作都有 HotGo 审计和 IM 审计,并可通过 Request ID 对应。
|
||||
4. 无权限接口返回 403,不能只依赖前端按钮隐藏。
|
||||
5. 数据库连接和 SQL 错误不泄露内部实现信息。
|
||||
|
||||
## 17. 完成标准
|
||||
|
||||
满足以下条件才能认为 HotGo 第 4 项完成:
|
||||
|
||||
1. 全局注册短信开关可由平台超级管理员配置,关闭值正确保存为 `0`。
|
||||
2. IM 服务能在约定缓存时间内读取开关,缺失和异常时采用安全默认值。
|
||||
3. HotGo 管理员身份能可靠映射到 `im_tenant.id`,不存在默认租户回退。
|
||||
4. 邀请码单个、批量、查询、详情、撤销和导出全部通过所选租户库完成。
|
||||
5. 平台与租户权限、跨租户隔离和服务端复核全部通过测试。
|
||||
6. 邀请码明文只出现在授权页面和授权导出中,不进入普通日志。
|
||||
7. 配置修改和邀请码敏感操作均有可关联的审计记录。
|
||||
8. 后端 Go 测试、前端类型检查/构建和核心页面联调通过。
|
||||
|
||||
## 18. 新会话可直接使用的任务说明
|
||||
|
||||
在新的开发会话中可直接发送:
|
||||
|
||||
```text
|
||||
请修改 HotGo 项目 /Users/youshunlai/go_workspace/im_manager。
|
||||
先完整阅读 /Users/youshunlai/go_workspace/im_server/docs/IM-HotGo注册短信开关与邀请码管理改造交接文档.md,
|
||||
再阅读其中列出的上游 IM 文档。按照交接文档的开发顺序完成 HotGo 的注册短信全局开关、
|
||||
统一租户身份、租户库直连、邀请码管理后台、菜单权限、日志脱敏和审计。
|
||||
只允许通过服务端 `im_tenant` 注册信息选择租户库,不要信任浏览器自报 tenant_id 或 DSN,
|
||||
不要修改 `im_registration_invite_uses`,不要把邀请码明文写入普通日志。
|
||||
开始前先检查 im_manager 当前工作区已有改动,并保留与本任务无关的用户修改。
|
||||
```
|
||||
@@ -0,0 +1,713 @@
|
||||
-- IM product database changes summary, chapters 1-11.
|
||||
-- Target:
|
||||
-- public backend database: hotgo
|
||||
-- default tenant business database: tinode
|
||||
-- Notes:
|
||||
-- 1. Public tenant registry tables live in hotgo.
|
||||
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
|
||||
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
|
||||
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Public database: tenant registry.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
tenant_desc VARCHAR(256),
|
||||
db_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
|
||||
state SMALLINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_code (code),
|
||||
INDEX idx_im_tenant_db_name (db_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_by BIGINT NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
updated_by BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id),
|
||||
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 1, tenant-aware login, verification, security.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
|
||||
user_id BIGINT NOT NULL,
|
||||
auth_version INT NOT NULL DEFAULT 1,
|
||||
password_updated_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
scene VARCHAR(32) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL DEFAULT '',
|
||||
phone_hash CHAR(64) NOT NULL,
|
||||
phone_mask VARCHAR(32) NOT NULL,
|
||||
target_user BIGINT,
|
||||
code_hash CHAR(64) NOT NULL,
|
||||
verification_token_hash CHAR(64),
|
||||
state SMALLINT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
resend_after DATETIME(3) NOT NULL,
|
||||
verified_at DATETIME(3),
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_verification_challenge (challenge_id),
|
||||
INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
name_cipher TEXT NOT NULL,
|
||||
id_type VARCHAR(32) NOT NULL,
|
||||
id_number_cipher TEXT NOT NULL,
|
||||
id_number_hash CHAR(64) NOT NULL,
|
||||
masked_name VARCHAR(64) NOT NULL,
|
||||
masked_id_number VARCHAR(64) NOT NULL,
|
||||
review_reason VARCHAR(256),
|
||||
verified_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_realname_submission (submission_id),
|
||||
INDEX idx_im_realname_user_latest (user_id, created_at),
|
||||
INDEX idx_im_realname_id_hash (id_number_hash)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target VARCHAR(128) NOT NULL DEFAULT '',
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
state SMALLINT NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
consumed_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_action_tokens_token (token_id),
|
||||
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
purpose VARCHAR(64) NOT NULL,
|
||||
password_hash VARBINARY(255) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 0,
|
||||
policy_version INT NOT NULL DEFAULT 1,
|
||||
restrictions JSON,
|
||||
enabled_at DATETIME(3),
|
||||
failed_attempts INT NOT NULL DEFAULT 0,
|
||||
locked_until DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, purpose)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
summary VARCHAR(512) NOT NULL,
|
||||
action JSON,
|
||||
read_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id),
|
||||
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
createdat DATETIME(3) NOT NULL,
|
||||
updatedat DATETIME(3) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seqid INT NOT NULL,
|
||||
version INT NOT NULL,
|
||||
actor BIGINT NOT NULL,
|
||||
head JSON,
|
||||
content JSON,
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
applicant_id BIGINT NOT NULL,
|
||||
target_id BIGINT NOT NULL,
|
||||
message VARCHAR(200) NOT NULL DEFAULT '',
|
||||
source VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_topic VARCHAR(64) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
handled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_friend_requests_request (request_id),
|
||||
UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key),
|
||||
INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at),
|
||||
INDEX idx_im_friend_requests_inbox (target_id, status, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
|
||||
user_id BIGINT NOT NULL,
|
||||
peer_id BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
remark VARCHAR(64) NOT NULL DEFAULT '',
|
||||
topic CHAR(25) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id, peer_id),
|
||||
INDEX idx_im_friendships_peer (peer_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
|
||||
blocker_id BIGINT NOT NULL,
|
||||
blocked_id BIGINT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (blocker_id, blocked_id),
|
||||
INDEX idx_im_user_blocks_blocked (blocked_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_favorites (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
favorite_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
item_type VARCHAR(16) NOT NULL,
|
||||
source_topic CHAR(25) NOT NULL,
|
||||
source_seq INT NOT NULL,
|
||||
snapshot JSON,
|
||||
attachment_refs JSON,
|
||||
availability VARCHAR(16) NOT NULL DEFAULT 'available',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
collected_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_favorites_favorite (favorite_id),
|
||||
UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq),
|
||||
UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))),
|
||||
INDEX idx_im_favorites_inbox (user_id, id DESC),
|
||||
INDEX idx_im_favorites_type (user_id, item_type, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- Existing Tinode tables must match the current schema for chapters 2-7:
|
||||
-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code.
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0,
|
||||
'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0,
|
||||
'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 8, realtime call records.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_call_records (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
call_id VARCHAR(64) NOT NULL,
|
||||
topic CHAR(25) NOT NULL,
|
||||
seq_id INT NOT NULL,
|
||||
caller_id BIGINT NOT NULL,
|
||||
callee_id BIGINT NOT NULL,
|
||||
call_type VARCHAR(16) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
end_reason VARCHAR(64) NOT NULL DEFAULT '',
|
||||
duration_ms BIGINT NOT NULL DEFAULT 0,
|
||||
started_at DATETIME(3) NOT NULL,
|
||||
accepted_at DATETIME(3),
|
||||
finished_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_call_records_call (call_id),
|
||||
UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id),
|
||||
INDEX idx_im_call_records_caller (caller_id, id DESC),
|
||||
INDEX idx_im_call_records_callee (callee_id, id DESC),
|
||||
INDEX idx_im_call_records_status (status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 9, workbench and office collaboration.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
code VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
route VARCHAR(128) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
unread_key VARCHAR(64) NOT NULL DEFAULT '',
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_workbench_apps_code (code),
|
||||
INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_departments (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_departments_department (department_id),
|
||||
INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id),
|
||||
INDEX idx_im_org_departments_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_members (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
department_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
display_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
position VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
mobile VARCHAR(32) NOT NULL DEFAULT '',
|
||||
email VARCHAR(128) NOT NULL DEFAULT '',
|
||||
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
contactable TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id),
|
||||
INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id),
|
||||
INDEX idx_im_org_members_user (user_id),
|
||||
INDEX idx_im_org_members_display_name (display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
organizer_id BIGINT NOT NULL,
|
||||
title VARCHAR(200) NOT NULL,
|
||||
start_at DATETIME(3) NOT NULL,
|
||||
end_at DATETIME(3) NOT NULL,
|
||||
location VARCHAR(256) NOT NULL DEFAULT '',
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
|
||||
reminder_minutes JSON,
|
||||
idempotency_key VARCHAR(128),
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
canceled_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
|
||||
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
|
||||
INDEX idx_im_meetings_organizer (organizer_id, start_at),
|
||||
INDEX idx_im_meetings_state_time (state, start_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
response VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
response_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id),
|
||||
INDEX idx_im_meeting_participants_user_time (user_id, meeting_id),
|
||||
INDEX idx_im_meeting_participants_response (meeting_id, response),
|
||||
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_date DATE NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_daily_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date),
|
||||
INDEX idx_im_daily_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_daily_reports_date_status (report_date, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
author_id BIGINT NOT NULL,
|
||||
report_month CHAR(7) NOT NULL,
|
||||
work_summary TEXT NOT NULL,
|
||||
next_plan TEXT NOT NULL,
|
||||
attachments JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft',
|
||||
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
submitted_at DATETIME(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month),
|
||||
INDEX idx_im_monthly_reports_author_id (author_id, id DESC),
|
||||
INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_anonymous TINYINT NOT NULL DEFAULT 1,
|
||||
required_fields JSON,
|
||||
max_length INT NOT NULL DEFAULT 5000,
|
||||
attachment_policy JSON,
|
||||
notice_title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
notice_content TEXT NOT NULL,
|
||||
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
submission_id VARCHAR(64) NOT NULL,
|
||||
sender_id BIGINT NOT NULL,
|
||||
subject VARCHAR(200) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
anonymous TINYINT NOT NULL DEFAULT 0,
|
||||
attachment_file_ids JSON,
|
||||
notice_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
submitted_at DATETIME(3) NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id),
|
||||
UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key),
|
||||
INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC),
|
||||
INDEX idx_im_mailbox_submissions_status (status, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
|
||||
user_id BIGINT NOT NULL,
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
birth_date DATE,
|
||||
signature VARCHAR(256) NOT NULL DEFAULT '',
|
||||
gender VARCHAR(16) NOT NULL DEFAULT '',
|
||||
region VARCHAR(128) NOT NULL DEFAULT '',
|
||||
public_account VARCHAR(64) NOT NULL DEFAULT '',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
|
||||
user_id BIGINT NOT NULL,
|
||||
add_me_methods JSON,
|
||||
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
|
||||
read_receipts TINYINT NOT NULL DEFAULT 1,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
platform VARCHAR(32) NOT NULL,
|
||||
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
|
||||
latest_version VARCHAR(64) NOT NULL,
|
||||
min_supported_version VARCHAR(64) NOT NULL,
|
||||
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
|
||||
release_notes TEXT NOT NULL,
|
||||
store_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
package_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
checksum VARCHAR(128) NOT NULL DEFAULT '',
|
||||
remind_after_seconds INT NOT NULL DEFAULT 0,
|
||||
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
|
||||
device_id VARCHAR(128) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
provider VARCHAR(32) NOT NULL,
|
||||
token VARCHAR(512) NOT NULL,
|
||||
app_version VARCHAR(64) NOT NULL DEFAULT '',
|
||||
platform VARCHAR(32) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (device_id),
|
||||
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
|
||||
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Tenant database: chapter 11, risk challenge, abuse report, feedback.
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
challenge_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
level VARCHAR(16) NOT NULL,
|
||||
notice_code VARCHAR(64) NOT NULL,
|
||||
notice_params JSON,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
decision VARCHAR(16) NOT NULL DEFAULT '',
|
||||
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id),
|
||||
INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
label VARCHAR(128) NOT NULL,
|
||||
requires_description TINYINT NOT NULL DEFAULT 0,
|
||||
allows_evidence TINYINT NOT NULL DEFAULT 1,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code),
|
||||
INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
report_id VARCHAR(64) NOT NULL,
|
||||
reporter_id BIGINT NOT NULL,
|
||||
target_type VARCHAR(16) NOT NULL,
|
||||
target_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
target_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_seq INT NOT NULL DEFAULT 0,
|
||||
reason_code VARCHAR(64) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
evidence_file_ids JSON,
|
||||
snapshot JSON,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_report (report_id),
|
||||
UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key),
|
||||
INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id),
|
||||
INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_config (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
categories JSON,
|
||||
max_length INT NOT NULL DEFAULT 1000,
|
||||
attachment_policy JSON,
|
||||
allow_contact TINYINT NOT NULL DEFAULT 1,
|
||||
diagnostics_notice TEXT NOT NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
feedback_id VARCHAR(64) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
category VARCHAR(64) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
contact VARCHAR(128) NOT NULL DEFAULT '',
|
||||
attachment_file_ids JSON,
|
||||
client_context JSON,
|
||||
include_diagnostics TINYINT NOT NULL DEFAULT 0,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'received',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id),
|
||||
UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key),
|
||||
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ============================================================
|
||||
-- Seed data.
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by)
|
||||
VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
db_name = VALUES(db_name),
|
||||
state = VALUES(state),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by)
|
||||
SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default'
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
|
||||
VALUES
|
||||
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
|
||||
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
|
||||
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
|
||||
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
|
||||
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
route = VALUES(route),
|
||||
sort_order = VALUES(sort_order),
|
||||
unread_key = VALUES(unread_key),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
|
||||
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
|
||||
VALUES
|
||||
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
|
||||
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
|
||||
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order)
|
||||
VALUES
|
||||
('user', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('user', 'harassment', '骚扰辱骂', 0, 1, 20),
|
||||
('user', 'illegal', '违法违规', 1, 1, 30),
|
||||
('group', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('group', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('group', 'illegal', '违法违规', 1, 1, 30),
|
||||
('message', 'fraud', '诈骗欺诈', 0, 1, 10),
|
||||
('message', 'spam', '垃圾广告', 0, 1, 20),
|
||||
('message', 'harassment', '骚扰辱骂', 0, 1, 30),
|
||||
('message', 'illegal', '违法违规', 1, 1, 40)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
label = VALUES(label),
|
||||
requires_description = VALUES(requires_description),
|
||||
allows_evidence = VALUES(allows_evidence),
|
||||
sort_order = VALUES(sort_order),
|
||||
updated_at = CURRENT_TIMESTAMP(3);
|
||||
|
||||
INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice)
|
||||
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
|
||||
ON DUPLICATE KEY UPDATE updated_at = updated_at;
|
||||
|
||||
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
|
||||
UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version';
|
||||
@@ -0,0 +1,33 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package imauditlog
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM审计日志列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imAuditLog/list" method:"get" tags:"IM审计日志" summary:"获取IM审计日志列表"`
|
||||
sysin.ImAuditLogListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImAuditLogListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM审计日志指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imAuditLog/view" method:"get" tags:"IM审计日志" summary:"获取IM审计日志指定信息"`
|
||||
sysin.ImAuditLogViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImAuditLogViewModel
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Package imgroup
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package imgroup
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM群组列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imGroup/list" method:"get" tags:"IM群组" summary:"获取IM群组列表"`
|
||||
sysin.ImGroupListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM群组指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imGroup/view" method:"get" tags:"IM群组" summary:"获取IM群组指定信息"`
|
||||
sysin.ImGroupViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImGroupViewModel
|
||||
}
|
||||
|
||||
// MemberListReq 查询IM群成员列表
|
||||
type MemberListReq struct {
|
||||
g.Meta `path:"/imGroup/memberList" method:"get" tags:"IM群组" summary:"获取IM群成员列表"`
|
||||
sysin.ImGroupMemberListInp
|
||||
}
|
||||
|
||||
type MemberListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupMemberListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// GovernanceReq 获取IM群治理配置
|
||||
type GovernanceReq struct {
|
||||
g.Meta `path:"/imGroup/governance" method:"get" tags:"IM群组" summary:"获取IM群治理配置"`
|
||||
sysin.ImGroupTopicInp
|
||||
}
|
||||
|
||||
type GovernanceRes struct {
|
||||
*sysin.ImGroupGovernanceModel
|
||||
}
|
||||
|
||||
// JoinRequestListReq 查询IM入群申请列表
|
||||
type JoinRequestListReq struct {
|
||||
g.Meta `path:"/imGroup/joinRequestList" method:"get" tags:"IM群组" summary:"获取IM入群申请列表"`
|
||||
sysin.ImGroupJoinRequestListInp
|
||||
}
|
||||
|
||||
type JoinRequestListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupJoinRequestListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// InvitationListReq 查询IM群邀请列表
|
||||
type InvitationListReq struct {
|
||||
g.Meta `path:"/imGroup/invitationList" method:"get" tags:"IM群组" summary:"获取IM群邀请列表"`
|
||||
sysin.ImGroupInvitationListInp
|
||||
}
|
||||
|
||||
type InvitationListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupInvitationListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// AnnouncementListReq 查询IM群公告列表
|
||||
type AnnouncementListReq struct {
|
||||
g.Meta `path:"/imGroup/announcementList" method:"get" tags:"IM群组" summary:"获取IM群公告列表"`
|
||||
sysin.ImGroupAnnouncementListInp
|
||||
}
|
||||
|
||||
type AnnouncementListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupAnnouncementListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// MuteListReq 查询IM群禁言列表
|
||||
type MuteListReq struct {
|
||||
g.Meta `path:"/imGroup/muteList" method:"get" tags:"IM群组" summary:"获取IM群禁言列表"`
|
||||
sysin.ImGroupMuteListInp
|
||||
}
|
||||
|
||||
type MuteListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupMuteListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// AdminLogListReq 查询IM群管理日志列表
|
||||
type AdminLogListReq struct {
|
||||
g.Meta `path:"/imGroup/adminLogList" method:"get" tags:"IM群组" summary:"获取IM群管理日志列表"`
|
||||
sysin.ImGroupAdminLogListInp
|
||||
}
|
||||
|
||||
type AdminLogListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImGroupAdminLogListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package iminvite
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imInvite/list" method:"post" tags:"IM邀请码" summary:"获取IM注册邀请码列表"`
|
||||
sysin.ImInviteListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImInviteCodeModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
type CreateReq struct {
|
||||
g.Meta `path:"/imInvite/create" method:"post" tags:"IM邀请码" summary:"生成IM注册邀请码"`
|
||||
sysin.ImInviteCreateInp
|
||||
}
|
||||
|
||||
type CreateRes struct {
|
||||
*sysin.ImInviteCodeModel
|
||||
}
|
||||
|
||||
type BatchCreateReq struct {
|
||||
g.Meta `path:"/imInvite/batchCreate" method:"post" tags:"IM邀请码" summary:"批量生成IM注册邀请码"`
|
||||
sysin.ImInviteBatchCreateInp
|
||||
}
|
||||
|
||||
type BatchCreateRes struct {
|
||||
*sysin.ImInviteBatchModel
|
||||
}
|
||||
|
||||
type BatchViewReq struct {
|
||||
g.Meta `path:"/imInvite/batchView" method:"get" tags:"IM邀请码" summary:"查看IM注册邀请码批次"`
|
||||
sysin.ImInviteBatchViewInp
|
||||
}
|
||||
|
||||
type BatchViewRes struct {
|
||||
*sysin.ImInviteBatchDetailModel
|
||||
}
|
||||
|
||||
type RevokeReq struct {
|
||||
g.Meta `path:"/imInvite/revoke" method:"post" tags:"IM邀请码" summary:"撤销IM注册邀请码"`
|
||||
sysin.ImInviteRevokeInp
|
||||
}
|
||||
|
||||
type RevokeRes struct{}
|
||||
|
||||
type BatchRevokeReq struct {
|
||||
g.Meta `path:"/imInvite/batchRevoke" method:"post" tags:"IM邀请码" summary:"批量撤销IM注册邀请码"`
|
||||
sysin.ImInviteBatchRevokeInp
|
||||
}
|
||||
|
||||
type BatchRevokeRes struct {
|
||||
*sysin.ImInviteBatchModel
|
||||
}
|
||||
|
||||
type ExportReq struct {
|
||||
g.Meta `path:"/imInvite/export" method:"post" tags:"IM邀请码" summary:"导出IM注册邀请码"`
|
||||
sysin.ImInviteListInp
|
||||
}
|
||||
|
||||
type ExportRes struct{}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Package imrealname
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package imrealname
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM实名审核列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imRealname/list" method:"get" tags:"IM实名审核" summary:"获取IM实名审核列表"`
|
||||
sysin.ImRealnameListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImRealnameListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM实名审核指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imRealname/view" method:"get" tags:"IM实名审核" summary:"获取IM实名审核指定信息"`
|
||||
sysin.ImRealnameViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImRealnameViewModel
|
||||
}
|
||||
|
||||
// ReviewReq 审核IM实名
|
||||
type ReviewReq struct {
|
||||
g.Meta `path:"/imRealname/review" method:"post" tags:"IM实名审核" summary:"审核IM实名"`
|
||||
sysin.ImRealnameReviewInp
|
||||
}
|
||||
|
||||
type ReviewRes struct{}
|
||||
@@ -0,0 +1,22 @@
|
||||
package imregistration
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type GetReq struct {
|
||||
g.Meta `path:"/imRegistration/config/get" method:"get" tags:"IM注册" summary:"获取IM注册配置"`
|
||||
}
|
||||
|
||||
type GetRes struct {
|
||||
*sysin.ImRegistrationConfigModel
|
||||
}
|
||||
|
||||
type UpdateReq struct {
|
||||
g.Meta `path:"/imRegistration/config/update" method:"post" tags:"IM注册" summary:"更新IM注册配置"`
|
||||
sysin.ImRegistrationConfigUpdateInp
|
||||
}
|
||||
|
||||
type UpdateRes struct{}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Package imtenant
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM租户列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imTenant/list" method:"get" tags:"IM租户" summary:"获取IM租户列表"`
|
||||
sysin.ImTenantListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImTenantListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM租户指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imTenant/view" method:"get" tags:"IM租户" summary:"获取IM租户指定信息"`
|
||||
sysin.ImTenantViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImTenantViewModel
|
||||
}
|
||||
|
||||
// EditReq 修改/新增IM租户
|
||||
type EditReq struct {
|
||||
g.Meta `path:"/imTenant/edit" method:"post" tags:"IM租户" summary:"修改/新增IM租户"`
|
||||
sysin.ImTenantEditInp
|
||||
}
|
||||
|
||||
type EditRes struct{}
|
||||
|
||||
// StatusReq 更新IM租户状态
|
||||
type StatusReq struct {
|
||||
g.Meta `path:"/imTenant/status" method:"post" tags:"IM租户" summary:"更新IM租户状态"`
|
||||
sysin.ImTenantStatusInp
|
||||
}
|
||||
|
||||
type StatusRes struct{}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Package imuser
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package imuser
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM用户列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imUser/list" method:"get" tags:"IM用户" summary:"获取IM用户列表"`
|
||||
sysin.ImUserListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImUserListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM用户指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imUser/view" method:"get" tags:"IM用户" summary:"获取IM用户指定信息"`
|
||||
sysin.ImUserViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImUserViewModel
|
||||
}
|
||||
|
||||
type ResetPasswordReq struct {
|
||||
g.Meta `path:"/imUser/resetPassword" method:"post" tags:"IM用户" summary:"重置IM用户密码"`
|
||||
sysin.ImUserPasswordResetInp
|
||||
}
|
||||
|
||||
type ResetPasswordRes struct{}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Package imworkorder
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package imworkorder
|
||||
|
||||
import (
|
||||
"hotgo/internal/model/input/form"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ListReq 查询IM举报反馈工单列表
|
||||
type ListReq struct {
|
||||
g.Meta `path:"/imWorkOrder/list" method:"get" tags:"IM举报反馈" summary:"获取IM举报反馈工单列表"`
|
||||
sysin.ImWorkOrderListInp
|
||||
}
|
||||
|
||||
type ListRes struct {
|
||||
form.PageRes
|
||||
List []*sysin.ImWorkOrderListModel `json:"list" dc:"数据列表"`
|
||||
}
|
||||
|
||||
// ViewReq 获取IM举报反馈工单指定信息
|
||||
type ViewReq struct {
|
||||
g.Meta `path:"/imWorkOrder/view" method:"get" tags:"IM举报反馈" summary:"获取IM举报反馈工单指定信息"`
|
||||
sysin.ImWorkOrderViewInp
|
||||
}
|
||||
|
||||
type ViewRes struct {
|
||||
*sysin.ImWorkOrderViewModel
|
||||
}
|
||||
|
||||
// StatusReq 更新IM举报反馈工单状态
|
||||
type StatusReq struct {
|
||||
g.Meta `path:"/imWorkOrder/status" method:"post" tags:"IM举报反馈" summary:"更新IM举报反馈工单状态"`
|
||||
sysin.ImWorkOrderStatusInp
|
||||
}
|
||||
|
||||
type StatusRes struct{}
|
||||
+3
-2
@@ -15,6 +15,7 @@ require (
|
||||
github.com/go-pay/crypto v0.0.1
|
||||
github.com/go-pay/gopay v1.5.114
|
||||
github.com/go-pay/smap v0.0.2
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
|
||||
github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0
|
||||
github.com/gogf/gf/contrib/nosql/redis/v2 v2.10.0
|
||||
@@ -34,9 +35,11 @@ require (
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1202
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.1200
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.66
|
||||
github.com/tinode/snowflake v1.0.0
|
||||
github.com/ufilesdk-dev/ufile-gosdk v1.0.6
|
||||
github.com/xuri/excelize/v2 v2.9.1
|
||||
go.opentelemetry.io/otel v1.38.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/mod v0.26.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/tools v0.35.0
|
||||
@@ -80,7 +83,6 @@ require (
|
||||
github.com/go-pay/xlog v0.0.3 // indirect
|
||||
github.com/go-pay/xtime v0.0.2 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
@@ -147,7 +149,6 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.uber.org/atomic v1.5.1 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/image v0.25.0 // indirect
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
|
||||
@@ -435,6 +435,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/tinode/snowflake v1.0.0 h1:YciQ9ZKn1TrnvpS8yZErt044XJaxWVtR9aMO9rOZVOE=
|
||||
github.com/tinode/snowflake v1.0.0/go.mod h1:5JiaCe3o7QdDeyRcAeZBGVghwRS+ygt2CF/hxmAoptQ=
|
||||
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
||||
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
|
||||
@@ -22,7 +22,7 @@ gfcli:
|
||||
|
||||
gen:
|
||||
dao:
|
||||
- link: "mysql:root:oWxz*2=sb),ouW%p@tcp(64.118.137.182:53306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
- link: "${HGGEN_DAO_LINK}"
|
||||
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
|
||||
group: "default" # 分组 使用hotgo代码生成功能时必须填
|
||||
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Package consts
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("ImGroupStateOptions", "IM群组状态选项", ImGroupStateOptions)
|
||||
dict.RegisterEnums("ImGroupStatusOptions", "IM群业务状态选项", ImGroupStatusOptions)
|
||||
dict.RegisterEnums("ImGroupRoleOptions", "IM群成员角色选项", ImGroupRoleOptions)
|
||||
dict.RegisterEnums("ImGroupMemberStatusOptions", "IM群成员状态选项", ImGroupMemberStatusOptions)
|
||||
dict.RegisterEnums("ImGroupRequestStatusOptions", "IM入群申请状态选项", ImGroupRequestStatusOptions)
|
||||
dict.RegisterEnums("ImGroupMuteStatusOptions", "IM禁言状态选项", ImGroupMuteStatusOptions)
|
||||
dict.RegisterEnums("ImGroupAnnouncementStatusOptions", "IM群公告状态选项", ImGroupAnnouncementStatusOptions)
|
||||
dict.RegisterEnums("ImGroupOutboxStatusOptions", "IM群事件状态选项", ImGroupOutboxStatusOptions)
|
||||
}
|
||||
|
||||
var ImGroupStateOptions = []*model.Option{
|
||||
dict.GenSuccessOption(ImUserStateOk, "正常"),
|
||||
dict.GenErrorOption(ImUserStateSuspended, "已停用"),
|
||||
dict.GenDefaultOption(ImUserStateDeleted, "已删除"),
|
||||
dict.GenWarningOption(ImUserStateUndefined, "未定义"),
|
||||
}
|
||||
|
||||
const (
|
||||
ImGroupStatusActive = "active"
|
||||
ImGroupStatusDissolved = "dissolved"
|
||||
ImGroupStatusDisabled = "disabled"
|
||||
|
||||
ImGroupRoleOwner = "owner"
|
||||
ImGroupRoleAdmin = "admin"
|
||||
ImGroupRoleMember = "member"
|
||||
|
||||
ImGroupMemberStatusActive = "active"
|
||||
ImGroupMemberStatusRemoved = "removed"
|
||||
ImGroupMemberStatusLeft = "left"
|
||||
|
||||
ImGroupRequestStatusPending = "pending"
|
||||
ImGroupRequestStatusAccepted = "accepted"
|
||||
ImGroupRequestStatusRejected = "rejected"
|
||||
ImGroupRequestStatusExpired = "expired"
|
||||
ImGroupRequestStatusCanceled = "canceled"
|
||||
|
||||
ImGroupMuteStatusActive = "active"
|
||||
ImGroupMuteStatusRevoked = "revoked"
|
||||
ImGroupMuteStatusExpired = "expired"
|
||||
|
||||
ImGroupAnnouncementStatusCurrent = "current"
|
||||
ImGroupAnnouncementStatusHistory = "history"
|
||||
ImGroupAnnouncementStatusDeleted = "deleted"
|
||||
|
||||
ImGroupOutboxStatusPending = "pending"
|
||||
ImGroupOutboxStatusDelivered = "delivered"
|
||||
ImGroupOutboxStatusFailed = "failed"
|
||||
)
|
||||
|
||||
var ImGroupStatusOptions = []*model.Option{
|
||||
dict.GenSuccessOption(ImGroupStatusActive, "正常"),
|
||||
dict.GenDefaultOption(ImGroupStatusDissolved, "已解散"),
|
||||
dict.GenErrorOption(ImGroupStatusDisabled, "已停用"),
|
||||
}
|
||||
|
||||
var ImGroupRoleOptions = []*model.Option{
|
||||
dict.GenPrimaryOption(ImGroupRoleOwner, "群主"),
|
||||
dict.GenSuccessOption(ImGroupRoleAdmin, "管理员"),
|
||||
dict.GenDefaultOption(ImGroupRoleMember, "成员"),
|
||||
}
|
||||
|
||||
var ImGroupMemberStatusOptions = []*model.Option{
|
||||
dict.GenSuccessOption(ImGroupMemberStatusActive, "正常"),
|
||||
dict.GenDefaultOption(ImGroupMemberStatusRemoved, "已移除"),
|
||||
dict.GenWarningOption(ImGroupMemberStatusLeft, "已退出"),
|
||||
}
|
||||
|
||||
var ImGroupRequestStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption(ImGroupRequestStatusPending, "待处理"),
|
||||
dict.GenSuccessOption(ImGroupRequestStatusAccepted, "已同意"),
|
||||
dict.GenErrorOption(ImGroupRequestStatusRejected, "已拒绝"),
|
||||
dict.GenDefaultOption(ImGroupRequestStatusExpired, "已过期"),
|
||||
dict.GenDefaultOption(ImGroupRequestStatusCanceled, "已取消"),
|
||||
}
|
||||
|
||||
var ImGroupMuteStatusOptions = []*model.Option{
|
||||
dict.GenErrorOption(ImGroupMuteStatusActive, "禁言中"),
|
||||
dict.GenDefaultOption(ImGroupMuteStatusRevoked, "已解除"),
|
||||
dict.GenWarningOption(ImGroupMuteStatusExpired, "已过期"),
|
||||
}
|
||||
|
||||
var ImGroupAnnouncementStatusOptions = []*model.Option{
|
||||
dict.GenSuccessOption(ImGroupAnnouncementStatusCurrent, "当前"),
|
||||
dict.GenDefaultOption(ImGroupAnnouncementStatusHistory, "历史"),
|
||||
dict.GenErrorOption(ImGroupAnnouncementStatusDeleted, "已删除"),
|
||||
}
|
||||
|
||||
var ImGroupOutboxStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption(ImGroupOutboxStatusPending, "待投递"),
|
||||
dict.GenSuccessOption(ImGroupOutboxStatusDelivered, "已投递"),
|
||||
dict.GenErrorOption(ImGroupOutboxStatusFailed, "投递失败"),
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Package consts
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("ImRealnameStatusOptions", "IM实名状态选项", ImRealnameStatusOptions)
|
||||
dict.RegisterEnums("ImWorkOrderStatusOptions", "IM工单状态选项", ImWorkOrderStatusOptions)
|
||||
}
|
||||
|
||||
const (
|
||||
ImRealnameStatusPending = "pending"
|
||||
ImRealnameStatusVerified = "verified"
|
||||
ImRealnameStatusRejected = "rejected"
|
||||
)
|
||||
|
||||
var ImRealnameStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption(ImRealnameStatusPending, "待审核"),
|
||||
dict.GenSuccessOption(ImRealnameStatusVerified, "已通过"),
|
||||
dict.GenErrorOption(ImRealnameStatusRejected, "已拒绝"),
|
||||
}
|
||||
|
||||
const (
|
||||
ImWorkOrderStatusReceived = "received"
|
||||
ImWorkOrderStatusProcessing = "processing"
|
||||
ImWorkOrderStatusResolved = "resolved"
|
||||
ImWorkOrderStatusRejected = "rejected"
|
||||
ImWorkOrderStatusClosed = "closed"
|
||||
)
|
||||
|
||||
var ImWorkOrderStatusOptions = []*model.Option{
|
||||
dict.GenWarningOption(ImWorkOrderStatusReceived, "待处理"),
|
||||
dict.GenPrimaryOption(ImWorkOrderStatusProcessing, "处理中"),
|
||||
dict.GenSuccessOption(ImWorkOrderStatusResolved, "已解决"),
|
||||
dict.GenErrorOption(ImWorkOrderStatusRejected, "已驳回"),
|
||||
dict.GenDefaultOption(ImWorkOrderStatusClosed, "已关闭"),
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Package consts
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("ImTenantStateOptions", "IM租户状态选项", ImTenantStateOptions)
|
||||
}
|
||||
|
||||
const (
|
||||
ImTenantStateProvisioning = 0 // 初始化中
|
||||
ImTenantStateActive = 1 // 正常
|
||||
ImTenantStateSuspended = 2 // 已停用
|
||||
ImTenantStateDeleting = 3 // 删除中
|
||||
ImTenantStateDeleted = 4 // 已删除
|
||||
)
|
||||
|
||||
var ImTenantStateOptions = []*model.Option{
|
||||
dict.GenWarningOption(ImTenantStateProvisioning, "初始化中"),
|
||||
dict.GenSuccessOption(ImTenantStateActive, "正常"),
|
||||
dict.GenErrorOption(ImTenantStateSuspended, "已停用"),
|
||||
dict.GenWarningOption(ImTenantStateDeleting, "删除中"),
|
||||
dict.GenDefaultOption(ImTenantStateDeleted, "已删除"),
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Package consts
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package consts
|
||||
|
||||
import (
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
dict.RegisterEnums("ImUserStateOptions", "IM用户状态选项", ImUserStateOptions)
|
||||
}
|
||||
|
||||
const (
|
||||
ImUserStateOk = 0 // 正常
|
||||
ImUserStateSuspended = 10 // 已停用
|
||||
ImUserStateDeleted = 20 // 已删除
|
||||
ImUserStateUndefined = 30 // 未定义
|
||||
)
|
||||
|
||||
var ImUserStateOptions = []*model.Option{
|
||||
dict.GenSuccessOption(ImUserStateOk, "正常"),
|
||||
dict.GenErrorOption(ImUserStateSuspended, "已停用"),
|
||||
dict.GenDefaultOption(ImUserStateDeleted, "已删除"),
|
||||
dict.GenWarningOption(ImUserStateUndefined, "未定义"),
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/i18n/gi18n"
|
||||
"hotgo/api/admin/common"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/captcha"
|
||||
@@ -93,7 +92,7 @@ func (c *cSite) LoginConfig(ctx context.Context, _ *common.SiteLoginConfigReq) (
|
||||
res.LoginConfig = login
|
||||
res.I18nSwitch = g.Cfg().MustGet(ctx, "system.i18n.switch", true).Bool()
|
||||
res.DefaultLanguage = g.Cfg().MustGet(ctx, "system.i18n.defaultLanguage", consts.SysDefaultLanguage).String()
|
||||
res.ProjectName = gi18n.T(ctx, "HotGo管理系统")
|
||||
res.ProjectName = "IM办公管理系统"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imauditlog"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var ImAuditLog = cImAuditLog{}
|
||||
|
||||
type cImAuditLog struct{}
|
||||
|
||||
// List 查看IM审计日志列表
|
||||
func (c *cImAuditLog) List(ctx context.Context, req *imauditlog.ListReq) (res *imauditlog.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImAuditLog().List(ctx, &req.ImAuditLogListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImAuditLogListModel{}
|
||||
}
|
||||
res = new(imauditlog.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM审计日志信息
|
||||
func (c *cImAuditLog) View(ctx context.Context, req *imauditlog.ViewReq) (res *imauditlog.ViewRes, err error) {
|
||||
data, err := service.SysImAuditLog().View(ctx, &req.ImAuditLogViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imauditlog.ViewRes)
|
||||
res.ImAuditLogViewModel = data
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imgroup"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ImGroup = cImGroup{}
|
||||
)
|
||||
|
||||
type cImGroup struct{}
|
||||
|
||||
// List 查看IM群组列表
|
||||
func (c *cImGroup) List(ctx context.Context, req *imgroup.ListReq) (res *imgroup.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().List(ctx, &req.ImGroupListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupListModel{}
|
||||
}
|
||||
|
||||
res = new(imgroup.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM群组信息
|
||||
func (c *cImGroup) View(ctx context.Context, req *imgroup.ViewReq) (res *imgroup.ViewRes, err error) {
|
||||
data, err := service.SysImGroup().View(ctx, &req.ImGroupViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(imgroup.ViewRes)
|
||||
res.ImGroupViewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// MemberList 查看IM群成员列表
|
||||
func (c *cImGroup) MemberList(ctx context.Context, req *imgroup.MemberListReq) (res *imgroup.MemberListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().MemberList(ctx, &req.ImGroupMemberListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupMemberListModel{}
|
||||
}
|
||||
|
||||
res = new(imgroup.MemberListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// Governance 获取IM群治理配置
|
||||
func (c *cImGroup) Governance(ctx context.Context, req *imgroup.GovernanceReq) (res *imgroup.GovernanceRes, err error) {
|
||||
data, err := service.SysImGroup().Governance(ctx, &req.ImGroupTopicInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imgroup.GovernanceRes)
|
||||
res.ImGroupGovernanceModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// JoinRequestList 查看IM入群申请列表
|
||||
func (c *cImGroup) JoinRequestList(ctx context.Context, req *imgroup.JoinRequestListReq) (res *imgroup.JoinRequestListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().JoinRequestList(ctx, &req.ImGroupJoinRequestListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupJoinRequestListModel{}
|
||||
}
|
||||
res = new(imgroup.JoinRequestListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// InvitationList 查看IM群邀请列表
|
||||
func (c *cImGroup) InvitationList(ctx context.Context, req *imgroup.InvitationListReq) (res *imgroup.InvitationListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().InvitationList(ctx, &req.ImGroupInvitationListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupInvitationListModel{}
|
||||
}
|
||||
res = new(imgroup.InvitationListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// AnnouncementList 查看IM群公告列表
|
||||
func (c *cImGroup) AnnouncementList(ctx context.Context, req *imgroup.AnnouncementListReq) (res *imgroup.AnnouncementListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().AnnouncementList(ctx, &req.ImGroupAnnouncementListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupAnnouncementListModel{}
|
||||
}
|
||||
res = new(imgroup.AnnouncementListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// MuteList 查看IM群禁言列表
|
||||
func (c *cImGroup) MuteList(ctx context.Context, req *imgroup.MuteListReq) (res *imgroup.MuteListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().MuteList(ctx, &req.ImGroupMuteListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupMuteListModel{}
|
||||
}
|
||||
res = new(imgroup.MuteListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// AdminLogList 查看IM群管理日志列表
|
||||
func (c *cImGroup) AdminLogList(ctx context.Context, req *imgroup.AdminLogListReq) (res *imgroup.AdminLogListRes, err error) {
|
||||
list, totalCount, err := service.SysImGroup().AdminLogList(ctx, &req.ImGroupAdminLogListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImGroupAdminLogListModel{}
|
||||
}
|
||||
res = new(imgroup.AdminLogListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/iminvite"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var ImInvite = cImInvite{}
|
||||
|
||||
type cImInvite struct{}
|
||||
|
||||
func (c *cImInvite) List(ctx context.Context, req *iminvite.ListReq) (res *iminvite.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImInvite().List(ctx, &req.ImInviteListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImInviteCodeModel{}
|
||||
}
|
||||
res = new(iminvite.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) Create(ctx context.Context, req *iminvite.CreateReq) (res *iminvite.CreateRes, err error) {
|
||||
data, err := service.SysImInvite().Create(ctx, &req.ImInviteCreateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(iminvite.CreateRes)
|
||||
res.ImInviteCodeModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) BatchCreate(ctx context.Context, req *iminvite.BatchCreateReq) (res *iminvite.BatchCreateRes, err error) {
|
||||
data, err := service.SysImInvite().BatchCreate(ctx, &req.ImInviteBatchCreateInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(iminvite.BatchCreateRes)
|
||||
res.ImInviteBatchModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) BatchView(ctx context.Context, req *iminvite.BatchViewReq) (res *iminvite.BatchViewRes, err error) {
|
||||
data, err := service.SysImInvite().BatchView(ctx, &req.ImInviteBatchViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(iminvite.BatchViewRes)
|
||||
res.ImInviteBatchDetailModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) Revoke(ctx context.Context, req *iminvite.RevokeReq) (res *iminvite.RevokeRes, err error) {
|
||||
err = service.SysImInvite().Revoke(ctx, &req.ImInviteRevokeInp)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) BatchRevoke(ctx context.Context, req *iminvite.BatchRevokeReq) (res *iminvite.BatchRevokeRes, err error) {
|
||||
data, err := service.SysImInvite().BatchRevoke(ctx, &req.ImInviteBatchRevokeInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(iminvite.BatchRevokeRes)
|
||||
res.ImInviteBatchModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImInvite) Export(ctx context.Context, req *iminvite.ExportReq) (res *iminvite.ExportRes, err error) {
|
||||
err = service.SysImInvite().Export(ctx, &req.ImInviteListInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imrealname"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ImRealname = cImRealname{}
|
||||
)
|
||||
|
||||
type cImRealname struct{}
|
||||
|
||||
// List 查看IM实名审核列表
|
||||
func (c *cImRealname) List(ctx context.Context, req *imrealname.ListReq) (res *imrealname.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImRealname().List(ctx, &req.ImRealnameListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImRealnameListModel{}
|
||||
}
|
||||
|
||||
res = new(imrealname.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM实名审核信息
|
||||
func (c *cImRealname) View(ctx context.Context, req *imrealname.ViewReq) (res *imrealname.ViewRes, err error) {
|
||||
data, err := service.SysImRealname().View(ctx, &req.ImRealnameViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(imrealname.ViewRes)
|
||||
res.ImRealnameViewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// Review 审核IM实名
|
||||
func (c *cImRealname) Review(ctx context.Context, req *imrealname.ReviewReq) (res *imrealname.ReviewRes, err error) {
|
||||
err = service.SysImRealname().Review(ctx, &req.ImRealnameReviewInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imregistration"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var ImRegistration = cImRegistration{}
|
||||
|
||||
type cImRegistration struct{}
|
||||
|
||||
func (c *cImRegistration) Get(ctx context.Context, req *imregistration.GetReq) (res *imregistration.GetRes, err error) {
|
||||
data, err := service.SysImRegistration().GetConfig(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res = new(imregistration.GetRes)
|
||||
res.ImRegistrationConfigModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImRegistration) Update(ctx context.Context, req *imregistration.UpdateReq) (res *imregistration.UpdateRes, err error) {
|
||||
err = service.SysImRegistration().UpdateConfig(ctx, &req.ImRegistrationConfigUpdateInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ImTenant = cImTenant{}
|
||||
)
|
||||
|
||||
type cImTenant struct{}
|
||||
|
||||
// List 查看IM租户列表
|
||||
func (c *cImTenant) List(ctx context.Context, req *imtenant.ListReq) (res *imtenant.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImTenant().List(ctx, &req.ImTenantListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*sysin.ImTenantListModel{}
|
||||
}
|
||||
|
||||
res = new(imtenant.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 更新IM租户
|
||||
func (c *cImTenant) Edit(ctx context.Context, req *imtenant.EditReq) (res *imtenant.EditRes, err error) {
|
||||
err = service.SysImTenant().Edit(ctx, &req.ImTenantEditInp)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM租户信息
|
||||
func (c *cImTenant) View(ctx context.Context, req *imtenant.ViewReq) (res *imtenant.ViewRes, err error) {
|
||||
data, err := service.SysImTenant().View(ctx, &req.ImTenantViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(imtenant.ViewRes)
|
||||
res.ImTenantViewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新IM租户状态
|
||||
func (c *cImTenant) Status(ctx context.Context, req *imtenant.StatusReq) (res *imtenant.StatusRes, err error) {
|
||||
err = service.SysImTenant().Status(ctx, &req.ImTenantStatusInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imuser"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ImUser = cImUser{}
|
||||
)
|
||||
|
||||
type cImUser struct{}
|
||||
|
||||
// List 查看IM用户列表
|
||||
func (c *cImUser) List(ctx context.Context, req *imuser.ListReq) (res *imuser.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImUser().List(ctx, &req.ImUserListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if list == nil {
|
||||
list = []*sysin.ImUserListModel{}
|
||||
}
|
||||
|
||||
res = new(imuser.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM用户信息
|
||||
func (c *cImUser) View(ctx context.Context, req *imuser.ViewReq) (res *imuser.ViewRes, err error) {
|
||||
data, err := service.SysImUser().View(ctx, &req.ImUserViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(imuser.ViewRes)
|
||||
res.ImUserViewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cImUser) ResetPassword(ctx context.Context, req *imuser.ResetPasswordReq) (res *imuser.ResetPasswordRes, err error) {
|
||||
err = service.SysImUser().ResetPassword(ctx, &req.ImUserPasswordResetInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/api/admin/imworkorder"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
)
|
||||
|
||||
var (
|
||||
ImWorkOrder = cImWorkOrder{}
|
||||
)
|
||||
|
||||
type cImWorkOrder struct{}
|
||||
|
||||
// List 查看IM举报反馈工单列表
|
||||
func (c *cImWorkOrder) List(ctx context.Context, req *imworkorder.ListReq) (res *imworkorder.ListRes, err error) {
|
||||
list, totalCount, err := service.SysImWorkOrder().List(ctx, &req.ImWorkOrderListInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []*sysin.ImWorkOrderListModel{}
|
||||
}
|
||||
|
||||
res = new(imworkorder.ListRes)
|
||||
res.List = list
|
||||
res.PageRes.Pack(req, totalCount)
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取指定IM举报反馈工单信息
|
||||
func (c *cImWorkOrder) View(ctx context.Context, req *imworkorder.ViewReq) (res *imworkorder.ViewRes, err error) {
|
||||
data, err := service.SysImWorkOrder().View(ctx, &req.ImWorkOrderViewInp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res = new(imworkorder.ViewRes)
|
||||
res.ImWorkOrderViewModel = data
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新IM举报反馈工单状态
|
||||
func (c *cImWorkOrder) Status(ctx context.Context, req *imworkorder.StatusReq) (res *imworkorder.StatusRes, err error) {
|
||||
err = service.SysImWorkOrder().Status(ctx, &req.ImWorkOrderStatusInp)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
|
||||
// =================================================================================
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"hotgo/internal/dao/internal"
|
||||
)
|
||||
|
||||
// imTenantDao is the data access object for the table im_tenant.
|
||||
// You can define custom methods on it to extend its functionality as needed.
|
||||
type imTenantDao struct {
|
||||
*internal.ImTenantDao
|
||||
}
|
||||
|
||||
var (
|
||||
// ImTenant is a globally accessible object for table im_tenant operations.
|
||||
ImTenant = imTenantDao{internal.NewImTenantDao()}
|
||||
)
|
||||
|
||||
// Add your custom methods and functionality below.
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
|
||||
// =================================================================================
|
||||
|
||||
package dao
|
||||
|
||||
import (
|
||||
"hotgo/internal/dao/internal"
|
||||
)
|
||||
|
||||
// imTenantConfigDao is the data access object for the table im_tenant_config.
|
||||
// You can define custom methods on it to extend its functionality as needed.
|
||||
type imTenantConfigDao struct {
|
||||
*internal.ImTenantConfigDao
|
||||
}
|
||||
|
||||
var (
|
||||
// ImTenantConfig is a globally accessible object for table im_tenant_config operations.
|
||||
ImTenantConfig = imTenantConfigDao{internal.NewImTenantConfigDao()}
|
||||
)
|
||||
|
||||
// Add your custom methods and functionality below.
|
||||
@@ -22,6 +22,7 @@ type AdminMemberDao struct {
|
||||
// AdminMemberColumns defines and stores column names for the table hg_admin_member.
|
||||
type AdminMemberColumns struct {
|
||||
Id string // 管理员ID
|
||||
TenantId string // IM租户ID
|
||||
DeptId string // 部门ID
|
||||
RoleId string // 角色ID
|
||||
RealName string // 真实姓名
|
||||
@@ -54,6 +55,7 @@ type AdminMemberColumns struct {
|
||||
// adminMemberColumns holds the columns for the table hg_admin_member.
|
||||
var adminMemberColumns = AdminMemberColumns{
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
DeptId: "dept_id",
|
||||
RoleId: "role_id",
|
||||
RealName: "real_name",
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ImTenantDao is the data access object for the table im_tenant.
|
||||
type ImTenantDao struct {
|
||||
table string // table is the underlying table name of the DAO.
|
||||
group string // group is the database configuration group name of the current DAO.
|
||||
columns ImTenantColumns // columns contains all the column names of Table for convenient usage.
|
||||
handlers []gdb.ModelHandler // handlers for customized model modification.
|
||||
}
|
||||
|
||||
// ImTenantColumns defines and stores column names for the table im_tenant.
|
||||
type ImTenantColumns struct {
|
||||
Id string //
|
||||
Code string //
|
||||
Name string //
|
||||
TenantDesc string //
|
||||
DbName string //
|
||||
DbDsn string //
|
||||
State string //
|
||||
CreatedAt string //
|
||||
CreatedBy string //
|
||||
UpdatedAt string //
|
||||
UpdatedBy string //
|
||||
}
|
||||
|
||||
// imTenantColumns holds the columns for the table im_tenant.
|
||||
var imTenantColumns = ImTenantColumns{
|
||||
Id: "id",
|
||||
Code: "code",
|
||||
Name: "name",
|
||||
TenantDesc: "tenant_desc",
|
||||
DbName: "db_name",
|
||||
DbDsn: "db_dsn",
|
||||
State: "state",
|
||||
CreatedAt: "created_at",
|
||||
CreatedBy: "created_by",
|
||||
UpdatedAt: "updated_at",
|
||||
UpdatedBy: "updated_by",
|
||||
}
|
||||
|
||||
// NewImTenantDao creates and returns a new DAO object for table data access.
|
||||
func NewImTenantDao(handlers ...gdb.ModelHandler) *ImTenantDao {
|
||||
return &ImTenantDao{
|
||||
group: "default",
|
||||
table: "im_tenant",
|
||||
columns: imTenantColumns,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
// DB retrieves and returns the underlying raw database management object of the current DAO.
|
||||
func (dao *ImTenantDao) DB() gdb.DB {
|
||||
return g.DB(dao.group)
|
||||
}
|
||||
|
||||
// Table returns the table name of the current DAO.
|
||||
func (dao *ImTenantDao) Table() string {
|
||||
return dao.table
|
||||
}
|
||||
|
||||
// Columns returns all column names of the current DAO.
|
||||
func (dao *ImTenantDao) Columns() ImTenantColumns {
|
||||
return dao.columns
|
||||
}
|
||||
|
||||
// Group returns the database configuration group name of the current DAO.
|
||||
func (dao *ImTenantDao) Group() string {
|
||||
return dao.group
|
||||
}
|
||||
|
||||
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
|
||||
func (dao *ImTenantDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
model := dao.DB().Model(dao.table)
|
||||
for _, handler := range dao.handlers {
|
||||
model = handler(model)
|
||||
}
|
||||
return model.Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// Transaction wraps the transaction logic using function f.
|
||||
// It rolls back the transaction and returns the error if function f returns a non-nil error.
|
||||
// It commits the transaction and returns nil if function f returns nil.
|
||||
//
|
||||
// Note: Do not commit or roll back the transaction in function f,
|
||||
// as it is automatically handled by this function.
|
||||
func (dao *ImTenantDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
|
||||
return dao.Ctx(ctx).Transaction(ctx, f)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
// ImTenantConfigDao is the data access object for the table im_tenant_config.
|
||||
type ImTenantConfigDao struct {
|
||||
table string // table is the underlying table name of the DAO.
|
||||
group string // group is the database configuration group name of the current DAO.
|
||||
columns ImTenantConfigColumns // columns contains all the column names of Table for convenient usage.
|
||||
handlers []gdb.ModelHandler // handlers for customized model modification.
|
||||
}
|
||||
|
||||
// ImTenantConfigColumns defines and stores column names for the table im_tenant_config.
|
||||
type ImTenantConfigColumns struct {
|
||||
Id string //
|
||||
TenantId string //
|
||||
MaxUsers string // 0 means unlimited
|
||||
MaxGroups string // 0 means unlimited
|
||||
MaxGroupMembers string // 0 means unlimited
|
||||
CreatedAt string //
|
||||
CreatedBy string //
|
||||
UpdatedAt string //
|
||||
UpdatedBy string //
|
||||
}
|
||||
|
||||
// imTenantConfigColumns holds the columns for the table im_tenant_config.
|
||||
var imTenantConfigColumns = ImTenantConfigColumns{
|
||||
Id: "id",
|
||||
TenantId: "tenant_id",
|
||||
MaxUsers: "max_users",
|
||||
MaxGroups: "max_groups",
|
||||
MaxGroupMembers: "max_group_members",
|
||||
CreatedAt: "created_at",
|
||||
CreatedBy: "created_by",
|
||||
UpdatedAt: "updated_at",
|
||||
UpdatedBy: "updated_by",
|
||||
}
|
||||
|
||||
// NewImTenantConfigDao creates and returns a new DAO object for table data access.
|
||||
func NewImTenantConfigDao(handlers ...gdb.ModelHandler) *ImTenantConfigDao {
|
||||
return &ImTenantConfigDao{
|
||||
group: "default",
|
||||
table: "im_tenant_config",
|
||||
columns: imTenantConfigColumns,
|
||||
handlers: handlers,
|
||||
}
|
||||
}
|
||||
|
||||
// DB retrieves and returns the underlying raw database management object of the current DAO.
|
||||
func (dao *ImTenantConfigDao) DB() gdb.DB {
|
||||
return g.DB(dao.group)
|
||||
}
|
||||
|
||||
// Table returns the table name of the current DAO.
|
||||
func (dao *ImTenantConfigDao) Table() string {
|
||||
return dao.table
|
||||
}
|
||||
|
||||
// Columns returns all column names of the current DAO.
|
||||
func (dao *ImTenantConfigDao) Columns() ImTenantConfigColumns {
|
||||
return dao.columns
|
||||
}
|
||||
|
||||
// Group returns the database configuration group name of the current DAO.
|
||||
func (dao *ImTenantConfigDao) Group() string {
|
||||
return dao.group
|
||||
}
|
||||
|
||||
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
|
||||
func (dao *ImTenantConfigDao) Ctx(ctx context.Context) *gdb.Model {
|
||||
model := dao.DB().Model(dao.table)
|
||||
for _, handler := range dao.handlers {
|
||||
model = handler(model)
|
||||
}
|
||||
return model.Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// Transaction wraps the transaction logic using function f.
|
||||
// It rolls back the transaction and returns the error if function f returns a non-nil error.
|
||||
// It commits the transaction and returns nil if function f returns nil.
|
||||
//
|
||||
// Note: Do not commit or roll back the transaction in function f,
|
||||
// as it is automatically handled by this function.
|
||||
func (dao *ImTenantConfigDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
|
||||
return dao.Ctx(ctx).Transaction(ctx, f)
|
||||
}
|
||||
@@ -78,6 +78,15 @@ func GetUserId(ctx context.Context) int64 {
|
||||
return user.Id
|
||||
}
|
||||
|
||||
// GetTenantId 获取当前身份关联的统一租户ID(im_tenant.id)。
|
||||
func GetTenantId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
if user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.TenantId
|
||||
}
|
||||
|
||||
// GetRoleId 获取用户角色ID
|
||||
func GetRoleId(ctx context.Context) int64 {
|
||||
user := GetUser(ctx)
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Package imtenant provides helpers for accessing tenant business databases.
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/model/entity"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
var tenantDbGroups sync.Map
|
||||
|
||||
const registryDbGroup = "imRegistry"
|
||||
|
||||
// DB resolves a tenant registry record and returns the corresponding business DB.
|
||||
func DB(ctx context.Context, tenantId int64) (db gdb.DB, tenant *entity.ImTenant, err error) {
|
||||
if tenantId <= 0 {
|
||||
return nil, nil, gerror.New("租户ID不能为空")
|
||||
}
|
||||
|
||||
if err = RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return nil, nil, gerror.Wrap(err, "获取租户信息失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
return nil, nil, gerror.New("租户不存在")
|
||||
}
|
||||
|
||||
link, err := tenantLink(ctx, tenant)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
group := fmt.Sprintf("im_tenant_%d_%x", tenant.Id, hash(link))
|
||||
if cached, ok := tenantDbGroups.Load(group); ok && cached == link {
|
||||
db, err = gdb.Instance(group)
|
||||
return db, tenant, err
|
||||
}
|
||||
|
||||
if err = gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
|
||||
return nil, nil, gerror.Wrap(err, "初始化租户业务库连接失败")
|
||||
}
|
||||
tenantDbGroups.Store(group, link)
|
||||
|
||||
db, err = gdb.Instance(group)
|
||||
if err != nil {
|
||||
return nil, nil, gerror.Wrap(err, "获取租户业务库连接失败")
|
||||
}
|
||||
return db, tenant, nil
|
||||
}
|
||||
|
||||
// RegistryDB returns the dedicated tenant registry connection, or the default
|
||||
// HotGo database when a separate registry group is not configured.
|
||||
func RegistryDB() gdb.DB {
|
||||
if configs, err := gdb.GetConfigGroup(registryDbGroup); err == nil && len(configs) > 0 {
|
||||
return g.DB(registryDbGroup)
|
||||
}
|
||||
return g.DB()
|
||||
}
|
||||
|
||||
func tenantLink(ctx context.Context, tenant *entity.ImTenant) (string, error) {
|
||||
if tenant.DbDsn != "" {
|
||||
return normalizeMySQLTimeOptions(tenant.DbDsn)
|
||||
}
|
||||
if tenant.DbName == "" {
|
||||
return "", gerror.New("租户业务库未配置")
|
||||
}
|
||||
|
||||
defaultLink := defaultDbLink(ctx)
|
||||
if defaultLink == "" {
|
||||
return "", gerror.New("默认数据库连接未配置,无法推导租户业务库连接")
|
||||
}
|
||||
|
||||
link, err := replaceMySQLDatabase(defaultLink, tenant.DbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return link, nil
|
||||
}
|
||||
|
||||
func defaultDbLink(ctx context.Context) string {
|
||||
if configs, err := gdb.GetConfigGroup("default"); err == nil && len(configs) > 0 {
|
||||
if configs[0].Link != "" {
|
||||
return configs[0].Link
|
||||
}
|
||||
}
|
||||
|
||||
cfg := g.Cfg()
|
||||
if !cfg.Available(ctx) {
|
||||
return ""
|
||||
}
|
||||
defaultLink := cfg.MustGet(ctx, "database.default.0.link").String()
|
||||
if defaultLink != "" {
|
||||
return defaultLink
|
||||
}
|
||||
return cfg.MustGet(ctx, "database.default.link").String()
|
||||
}
|
||||
|
||||
func replaceMySQLDatabase(link string, dbName string) (string, error) {
|
||||
if !strings.HasPrefix(link, "mysql:") {
|
||||
return "", gerror.New("仅支持从 MySQL 默认连接推导租户业务库连接")
|
||||
}
|
||||
|
||||
dbStart := strings.LastIndex(link, ")/")
|
||||
if dbStart < 0 {
|
||||
return "", gerror.New("默认数据库连接格式不正确")
|
||||
}
|
||||
dbStart += 2
|
||||
queryStart := strings.Index(link[dbStart:], "?")
|
||||
if queryStart < 0 {
|
||||
return normalizeMySQLTimeOptions(link[:dbStart] + dbName)
|
||||
}
|
||||
queryStart += dbStart
|
||||
return normalizeMySQLTimeOptions(link[:dbStart] + dbName + link[queryStart:])
|
||||
}
|
||||
|
||||
func normalizeMySQLTimeOptions(link string) (string, error) {
|
||||
if !strings.HasPrefix(link, "mysql:") {
|
||||
return "", gerror.New("租户业务库仅支持 MySQL")
|
||||
}
|
||||
parts := strings.SplitN(link, "?", 2)
|
||||
values := make(url.Values)
|
||||
if len(parts) == 2 {
|
||||
var err error
|
||||
values, err = url.ParseQuery(parts[1])
|
||||
if err != nil {
|
||||
return "", gerror.Wrap(err, "租户业务库连接参数格式不正确")
|
||||
}
|
||||
}
|
||||
values.Set("loc", "UTC")
|
||||
values.Set("parseTime", "true")
|
||||
return parts[0] + "?" + values.Encode(), nil
|
||||
}
|
||||
|
||||
func hash(s string) uint64 {
|
||||
h := fnv.New64a()
|
||||
_, _ = h.Write([]byte(s))
|
||||
return h.Sum64()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReplaceMySQLDatabaseUsesUTC(t *testing.T) {
|
||||
link, err := replaceMySQLDatabase("mysql:user:pass@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=false&charset=utf8mb4", "tinode")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, expected := range []string{"/tinode?", "loc=UTC", "parseTime=true", "charset=utf8mb4"} {
|
||||
if !strings.Contains(link, expected) {
|
||||
t.Fatalf("link %q does not contain %q", link, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextSnowflakeID(t *testing.T) {
|
||||
first, err := NextSnowflakeID(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := NextSnowflakeID(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first <= 0 || second <= first {
|
||||
t.Fatalf("Snowflake IDs are not increasing: %d, %d", first, second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package imtenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
sf "github.com/tinode/snowflake"
|
||||
)
|
||||
|
||||
const defaultSnowflakeWorkerID = 1023
|
||||
|
||||
var (
|
||||
snowflakeOnce sync.Once
|
||||
snowflakeGen *sf.SnowFlake
|
||||
snowflakeErr error
|
||||
)
|
||||
|
||||
// NextSnowflakeID returns an unencrypted Snowflake ID suitable for IM BIGINT keys.
|
||||
func NextSnowflakeID(ctx context.Context) (int64, error) {
|
||||
snowflakeOnce.Do(func() {
|
||||
workerID := g.Cfg().MustGet(ctx, "imDirectDb.snowflakeWorkerId", defaultSnowflakeWorkerID).Int()
|
||||
if value := os.Getenv("IM_DIRECT_DB_SNOWFLAKE_WORKER_ID"); value != "" {
|
||||
workerID, snowflakeErr = strconv.Atoi(value)
|
||||
if snowflakeErr != nil {
|
||||
snowflakeErr = fmt.Errorf("invalid IM_DIRECT_DB_SNOWFLAKE_WORKER_ID: %w", snowflakeErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
if workerID < 0 || workerID > 1023 {
|
||||
snowflakeErr = fmt.Errorf("IM Snowflake worker ID must be between 0 and 1023")
|
||||
return
|
||||
}
|
||||
snowflakeGen, snowflakeErr = sf.NewSnowFlake(uint32(workerID))
|
||||
})
|
||||
if snowflakeErr != nil {
|
||||
return 0, snowflakeErr
|
||||
}
|
||||
id, err := snowflakeGen.Next()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if id == 0 || id > uint64(^uint64(0)>>1) {
|
||||
return 0, fmt.Errorf("generated invalid IM Snowflake ID")
|
||||
}
|
||||
return int64(id), nil
|
||||
}
|
||||
@@ -467,6 +467,29 @@ func (s *sAdminMember) Edit(ctx context.Context, in *adminin.MemberEditInp) (err
|
||||
return
|
||||
}
|
||||
|
||||
if service.AdminMember().VerifySuperId(ctx, opMemberId) {
|
||||
if in.TenantId > 0 {
|
||||
count, queryErr := g.DB("imRegistry").Model(dao.ImTenant.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImTenant.Columns().Id, in.TenantId).
|
||||
Where(dao.ImTenant.Columns().State, consts.ImTenantStateActive).
|
||||
Count()
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "校验IM租户失败")
|
||||
}
|
||||
if count == 0 {
|
||||
return gerror.New("IM租户不存在或未启用")
|
||||
}
|
||||
}
|
||||
} else if deptType := contexts.GetDeptType(ctx); deptType == consts.DeptTypeTenant ||
|
||||
deptType == consts.DeptTypeMerchant || deptType == consts.DeptTypeUser {
|
||||
in.TenantId = contexts.GetTenantId(ctx)
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("当前管理员未关联租户")
|
||||
}
|
||||
} else {
|
||||
in.TenantId = 0
|
||||
}
|
||||
|
||||
cols := dao.AdminMember.Columns()
|
||||
err = s.VerifyUnique(ctx, &adminin.VerifyUniqueInp{
|
||||
Id: in.Id,
|
||||
|
||||
@@ -228,6 +228,7 @@ func (s *sAdminSite) handleLogin(ctx context.Context, mb *entity.AdminMember) (r
|
||||
|
||||
user := &model.Identity{
|
||||
Id: mb.Id,
|
||||
TenantId: mb.TenantId,
|
||||
Pid: mb.Pid,
|
||||
DeptId: dept.Id,
|
||||
DeptType: dept.Type,
|
||||
@@ -319,6 +320,7 @@ func (s *sAdminSite) BindUserContext(ctx context.Context, claims *model.Identity
|
||||
|
||||
user := &model.Identity{
|
||||
Id: mb.Id,
|
||||
TenantId: mb.TenantId,
|
||||
Pid: mb.Pid,
|
||||
DeptId: dept.Id,
|
||||
DeptType: dept.Type,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/service"
|
||||
"hotgo/utility/simple"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type imAdminScope struct {
|
||||
TenantId int64
|
||||
OperatorId int64
|
||||
RoleKey string
|
||||
RequestId string
|
||||
}
|
||||
|
||||
const (
|
||||
imAdminOperationAuditTable = "im_admin_operation_audit"
|
||||
)
|
||||
|
||||
func resolveImAdminScope(ctx context.Context, selectedTenantId int64) (scope imAdminScope, err error) {
|
||||
return resolveImAdminScopeWithAll(ctx, selectedTenantId, false)
|
||||
}
|
||||
|
||||
func resolveImAdminScopeWithAll(ctx context.Context, selectedTenantId int64, allowAll bool) (scope imAdminScope, err error) {
|
||||
operatorId := contexts.GetUserId(ctx)
|
||||
scope.OperatorId = operatorId
|
||||
scope.RoleKey = contexts.GetRoleKey(ctx)
|
||||
scope.RequestId = gctx.CtxId(ctx)
|
||||
if service.AdminMember().VerifySuperId(ctx, operatorId) {
|
||||
if selectedTenantId <= 0 {
|
||||
if allowAll {
|
||||
return scope, nil
|
||||
}
|
||||
return scope, gerror.New("请选择IM租户")
|
||||
}
|
||||
if err = ensureImTenantActive(ctx, selectedTenantId); err != nil {
|
||||
return scope, err
|
||||
}
|
||||
scope.TenantId = selectedTenantId
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
if contexts.GetDeptType(ctx) != consts.DeptTypeTenant {
|
||||
return scope, gerror.New("当前账号不是租户管理员,不能管理邀请码")
|
||||
}
|
||||
tenantId := contexts.GetTenantId(ctx)
|
||||
if tenantId <= 0 {
|
||||
return scope, gerror.New("当前管理员未关联租户")
|
||||
}
|
||||
if err = ensureImTenantActive(ctx, tenantId); err != nil {
|
||||
return scope, err
|
||||
}
|
||||
scope.TenantId = tenantId
|
||||
return scope, nil
|
||||
}
|
||||
|
||||
func ensureImTenantActive(ctx context.Context, tenantId int64) (err error) {
|
||||
var tenant *entity.ImTenant
|
||||
if err = imtenant.RegistryDB().Model(dao.ImTenant.Table()).Safe().Ctx(ctx).WherePri(tenantId).Scan(&tenant); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户失败")
|
||||
}
|
||||
if tenant == nil || tenant.Id <= 0 {
|
||||
return gerror.New("IM租户不存在")
|
||||
}
|
||||
if tenant.State != consts.ImTenantStateActive {
|
||||
return gerror.New("IM租户未启用")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeImAdminAudit(ctx context.Context, tenantId int64, action string, targetType string, targetId int64, result string, detail g.Map) error {
|
||||
operatorId := contexts.GetUserId(ctx)
|
||||
request := ghttp.RequestFromCtx(ctx)
|
||||
ip := ""
|
||||
if request != nil {
|
||||
ip = request.GetClientIp()
|
||||
}
|
||||
detailJson := gjson.New(detail)
|
||||
_, err := g.DB().Exec(ctx,
|
||||
"INSERT INTO "+imAdminOperationAuditTable+" (request_id, tenant_id, operator_id, action, target_type, target_id, result, ip, detail_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?)",
|
||||
gctx.CtxId(ctx),
|
||||
tenantId,
|
||||
operatorId,
|
||||
action,
|
||||
targetType,
|
||||
targetId,
|
||||
result,
|
||||
ip,
|
||||
detailJson.String(),
|
||||
gtime.Now(),
|
||||
)
|
||||
if err != nil && !simple.IsDemo(ctx) {
|
||||
g.Log().Warningf(ctx, "写入IM后台审计失败 action:%s err:%+v", action, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sSysImAuditLog struct{}
|
||||
|
||||
func NewSysImAuditLog() *sSysImAuditLog {
|
||||
return &sSysImAuditLog{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImAuditLog(NewSysImAuditLog())
|
||||
}
|
||||
|
||||
// List 获取IM审计日志列表
|
||||
func (s *sSysImAuditLog) List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.model(ctx, db)
|
||||
mod = s.applyWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("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
|
||||
}
|
||||
|
||||
func (s *sSysImAuditLog) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model("im_audit_logs").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"actor_uid",
|
||||
"actor_role",
|
||||
"action",
|
||||
"target_type",
|
||||
"target_id",
|
||||
"reason",
|
||||
"request_id",
|
||||
"ip",
|
||||
"user_agent",
|
||||
"CAST(before_snapshot AS CHAR) before_snapshot",
|
||||
"CAST(after_snapshot AS CHAR) after_snapshot",
|
||||
"CAST(metadata AS CHAR) metadata",
|
||||
"created_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImAuditLog) applyWhere(mod *gdb.Model, in *sysin.ImAuditLogListInp) *gdb.Model {
|
||||
if in.ActorUid > 0 {
|
||||
mod = mod.Where("actor_uid", in.ActorUid)
|
||||
}
|
||||
if in.ActorRole != "" {
|
||||
mod = mod.WhereLike("actor_role", "%"+in.ActorRole+"%")
|
||||
}
|
||||
if in.Action != "" {
|
||||
mod = mod.WhereLike("action", "%"+in.Action+"%")
|
||||
}
|
||||
if in.TargetType != "" {
|
||||
mod = mod.Where("target_type", in.TargetType)
|
||||
}
|
||||
if in.TargetId != "" {
|
||||
mod = mod.WhereLike("target_id", "%"+in.TargetId+"%")
|
||||
}
|
||||
if in.RequestId != "" {
|
||||
mod = mod.WhereLike("request_id", "%"+in.RequestId+"%")
|
||||
}
|
||||
if in.Ip != "" {
|
||||
mod = mod.WhereLike("ip", "%"+in.Ip+"%")
|
||||
}
|
||||
if in.Keyword != "" {
|
||||
like := "%" + in.Keyword + "%"
|
||||
mod = mod.Where("(action LIKE ? OR target_type LIKE ? OR target_id LIKE ? OR reason LIKE ? OR request_id LIKE ?)", like, like, like, like, like)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// View 获取IM审计日志指定信息
|
||||
func (s *sSysImAuditLog) View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.model(ctx, db).Where("id", in.Id).Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM审计日志信息失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
|
||||
type sSysImGroup struct{}
|
||||
|
||||
func NewSysImGroup() *sSysImGroup {
|
||||
return &sSysImGroup{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImGroup(NewSysImGroup())
|
||||
}
|
||||
|
||||
// List 获取IM群组列表
|
||||
func (s *sSysImGroup) List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.groupModel(ctx, db)
|
||||
mod = s.applyGroupWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("t.touchedat").OrderDesc("t.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
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) groupModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model("im_topics t").Safe().Ctx(ctx).
|
||||
LeftJoin("im_group_settings gs", "gs.topic = t.name").
|
||||
Fields(
|
||||
"t.name topic",
|
||||
"t.state",
|
||||
"COALESCE(gs.owner_user_id, t.owner) owner_id",
|
||||
"t.seqid seq_id",
|
||||
"t.delid del_id",
|
||||
"t.subcnt sub_cnt",
|
||||
"t.touchedat touched_at",
|
||||
"t.createdat created_at",
|
||||
"t.updatedat updated_at",
|
||||
"t.public public_json",
|
||||
"t.trusted trusted_json",
|
||||
"t.tags tags_json",
|
||||
"t.aux aux_json",
|
||||
"COALESCE(gs.name, '') group_name",
|
||||
"COALESCE(gs.avatar_file_id, '') avatar_file_id",
|
||||
"COALESCE(gs.description, '') description",
|
||||
"COALESCE(gs.group_status, '') group_status",
|
||||
"COALESCE(gs.join_approval, 0) join_approval",
|
||||
"COALESCE(gs.mute_all, 0) mute_all",
|
||||
"COALESCE(gs.member_upload_files, 0) member_upload_files",
|
||||
"COALESCE(gs.member_private_chat, 0) member_private_chat",
|
||||
"COALESCE(gs.member_invite, 0) member_invite",
|
||||
"COALESCE(gs.max_member_count, 0) max_member_count",
|
||||
"COALESCE(gs.max_admin_count, 0) max_admin_count",
|
||||
"COALESCE(gs.member_count, 0) business_member_count",
|
||||
"COALESCE(gs.version, 0) settings_version",
|
||||
"gs.updated_at settings_updated_at",
|
||||
).
|
||||
WhereLike("t.name", "grp%")
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) applyGroupWhere(mod *gdb.Model, in *sysin.ImGroupListInp) *gdb.Model {
|
||||
if in.Topic != "" {
|
||||
mod = mod.WhereLike("t.name", "%"+in.Topic+"%")
|
||||
}
|
||||
if in.OwnerId > 0 {
|
||||
mod = mod.Where("COALESCE(gs.owner_user_id, t.owner)", in.OwnerId)
|
||||
}
|
||||
if in.State != nil {
|
||||
mod = mod.Where("t.state", *in.State)
|
||||
}
|
||||
if in.GroupStatus != "" {
|
||||
mod = mod.Where("gs.group_status", in.GroupStatus)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("t.createdat", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
if len(in.TouchedAt) == 2 {
|
||||
mod = mod.WhereBetween("t.touchedat", in.TouchedAt[0], in.TouchedAt[1])
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// View 获取IM群组指定信息
|
||||
func (s *sSysImGroup) View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Model("im_topics t").Safe().Ctx(ctx).
|
||||
LeftJoin("im_group_settings gs", "gs.topic = t.name").
|
||||
Fields(
|
||||
"t.name topic",
|
||||
"t.state",
|
||||
"COALESCE(gs.owner_user_id, t.owner) owner_id",
|
||||
"t.seqid seq_id",
|
||||
"t.delid del_id",
|
||||
"t.subcnt sub_cnt",
|
||||
"t.touchedat touched_at",
|
||||
"t.createdat created_at",
|
||||
"t.updatedat updated_at",
|
||||
"t.public public_json",
|
||||
"t.trusted trusted_json",
|
||||
"t.tags tags_json",
|
||||
"t.aux aux_json",
|
||||
"t.access access_json",
|
||||
"t.usebt use_bt",
|
||||
"COALESCE(gs.name, '') group_name",
|
||||
"COALESCE(gs.avatar_file_id, '') avatar_file_id",
|
||||
"COALESCE(gs.description, '') description",
|
||||
"COALESCE(gs.group_status, '') group_status",
|
||||
"COALESCE(gs.join_approval, 0) join_approval",
|
||||
"COALESCE(gs.mute_all, 0) mute_all",
|
||||
"COALESCE(gs.member_upload_files, 0) member_upload_files",
|
||||
"COALESCE(gs.member_private_chat, 0) member_private_chat",
|
||||
"COALESCE(gs.member_invite, 0) member_invite",
|
||||
"COALESCE(gs.max_member_count, 0) max_member_count",
|
||||
"COALESCE(gs.max_admin_count, 0) max_admin_count",
|
||||
"COALESCE(gs.member_count, 0) business_member_count",
|
||||
"COALESCE(gs.version, 0) settings_version",
|
||||
"gs.updated_at settings_updated_at",
|
||||
).
|
||||
Where("t.name", in.Topic).
|
||||
WhereLike("t.name", "grp%").
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM群组信息失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
return
|
||||
}
|
||||
|
||||
// MemberList 获取IM群成员列表
|
||||
func (s *sSysImGroup) MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if s.hasGroupMemberTruth(ctx, db, in.Topic) {
|
||||
return s.memberListFromBusiness(ctx, db, in)
|
||||
}
|
||||
return s.memberListFromProjection(ctx, db, in)
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) hasGroupMemberTruth(ctx context.Context, db gdb.DB, topic string) bool {
|
||||
count, err := db.Model("im_group_members").Safe().Ctx(ctx).Where("topic", topic).Count()
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) memberListFromBusiness(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
||||
mod := db.Model("im_group_members gm").Safe().Ctx(ctx).
|
||||
LeftJoin("im_users u", "u.id = gm.user_id").
|
||||
LeftJoin("im_user_profiles up", "up.user_id = gm.user_id").
|
||||
LeftJoin("im_subscriptions s", "s.topic = gm.topic AND s.userid = gm.user_id").
|
||||
Fields(
|
||||
"gm.user_id",
|
||||
"up.nickname",
|
||||
"u.state",
|
||||
"s.modewant mode_want",
|
||||
"s.modegiven mode_given",
|
||||
"s.recvseqid recv_seq_id",
|
||||
"s.readseqid read_seq_id",
|
||||
"s.delid del_id",
|
||||
"s.deletedat deleted_at",
|
||||
"s.createdat created_at",
|
||||
"s.updatedat updated_at",
|
||||
"s.private private_json",
|
||||
"gm.role",
|
||||
"gm.member_status",
|
||||
"gm.nickname group_nickname",
|
||||
"gm.pinned",
|
||||
"gm.mute_notifications",
|
||||
"gm.join_source",
|
||||
"gm.joined_by",
|
||||
"gm.joined_at",
|
||||
"gm.left_at",
|
||||
"gm.removed_by",
|
||||
"gm.version",
|
||||
).
|
||||
Where("gm.topic", in.Topic)
|
||||
if in.UserId > 0 {
|
||||
mod = mod.Where("gm.user_id", in.UserId)
|
||||
}
|
||||
if in.Role != "" {
|
||||
mod = mod.Where("gm.role", in.Role)
|
||||
}
|
||||
if in.MemberStatus != "" {
|
||||
mod = mod.Where("gm.member_status", in.MemberStatus)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("gm.member_status").OrderAsc("gm.role").OrderAsc("gm.id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImGroup) memberListFromProjection(ctx context.Context, db gdb.DB, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error) {
|
||||
mod := db.Model("im_subscriptions s").Safe().Ctx(ctx).
|
||||
LeftJoin("im_users u", "u.id = s.userid").
|
||||
LeftJoin("im_user_profiles up", "up.user_id = s.userid").
|
||||
Fields(
|
||||
"s.userid user_id",
|
||||
"up.nickname",
|
||||
"u.state",
|
||||
"s.modewant mode_want",
|
||||
"s.modegiven mode_given",
|
||||
"s.recvseqid recv_seq_id",
|
||||
"s.readseqid read_seq_id",
|
||||
"s.delid del_id",
|
||||
"s.deletedat deleted_at",
|
||||
"s.createdat created_at",
|
||||
"s.updatedat updated_at",
|
||||
"s.private private_json",
|
||||
"'member' role",
|
||||
"IF(s.deletedat IS NULL, 'active', 'left') member_status",
|
||||
"'' group_nickname",
|
||||
"0 pinned",
|
||||
"0 mute_notifications",
|
||||
"'projection' join_source",
|
||||
"0 joined_by",
|
||||
"s.createdat joined_at",
|
||||
"s.deletedat left_at",
|
||||
"0 removed_by",
|
||||
"0 version",
|
||||
).
|
||||
Where("s.topic", in.Topic)
|
||||
|
||||
if in.UserId > 0 {
|
||||
mod = mod.Where("s.userid", in.UserId)
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderAsc("s.deletedat").OrderAsc("s.userid")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群成员列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Governance 获取IM群治理配置
|
||||
func (s *sSysImGroup) Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = db.Model("im_group_settings").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"topic",
|
||||
"owner_user_id",
|
||||
"name",
|
||||
"avatar_file_id",
|
||||
"description",
|
||||
"group_status",
|
||||
"join_approval",
|
||||
"mute_all",
|
||||
"member_upload_files",
|
||||
"member_private_chat",
|
||||
"member_invite",
|
||||
"max_member_count",
|
||||
"max_admin_count",
|
||||
"member_count",
|
||||
"version",
|
||||
"created_by",
|
||||
"dissolved_by",
|
||||
"dissolved_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
Where("topic", in.Topic).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM群治理配置失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
return
|
||||
}
|
||||
|
||||
// JoinRequestList 获取IM入群申请列表
|
||||
func (s *sSysImGroup) JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model("im_group_join_requests").Safe().Ctx(ctx).Where("topic", in.Topic)
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if in.ApplicantUserId > 0 {
|
||||
mod = mod.Where("applicant_user_id", in.ApplicantUserId)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM入群申请列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// InvitationList 获取IM群邀请列表
|
||||
func (s *sSysImGroup) InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model("im_group_invitations").Safe().Ctx(ctx).Where("topic", in.Topic)
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if in.TargetUserId > 0 {
|
||||
mod = mod.Where("target_user_id", in.TargetUserId)
|
||||
}
|
||||
if in.InviterUserId > 0 {
|
||||
mod = mod.Where("inviter_user_id", in.InviterUserId)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群邀请列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AnnouncementList 获取IM群公告列表
|
||||
func (s *sSysImGroup) AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model("im_group_announcements").Safe().Ctx(ctx).Where("topic", in.Topic)
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("published_at").OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群公告列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MuteList 获取IM群禁言列表
|
||||
func (s *sSysImGroup) MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model("im_group_mutes").Safe().Ctx(ctx).Where("topic", in.Topic)
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if in.UserId > 0 {
|
||||
mod = mod.Where("user_id", in.UserId)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("muted_at").OrderDesc("user_id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群禁言列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// AdminLogList 获取IM群管理日志列表
|
||||
func (s *sSysImGroup) AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
mod := db.Model("im_group_admin_logs").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"log_id",
|
||||
"topic",
|
||||
"operator_id",
|
||||
"target_user_id",
|
||||
"action",
|
||||
"object_type",
|
||||
"object_id",
|
||||
"CAST(before_data AS CHAR) before_data",
|
||||
"CAST(after_data AS CHAR) after_data",
|
||||
"request_id",
|
||||
"idempotency_key",
|
||||
"created_at",
|
||||
).
|
||||
Where("topic", in.Topic)
|
||||
if in.Action != "" {
|
||||
mod = mod.WhereLike("action", "%"+in.Action+"%")
|
||||
}
|
||||
if in.OperatorId > 0 {
|
||||
mod = mod.Where("operator_id", in.OperatorId)
|
||||
}
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("id")
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM群管理日志列表失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const inviteGenerateRetries = 10
|
||||
|
||||
var errInviteBatchNotFound = errors.New("invite batch not found")
|
||||
|
||||
type sSysImInvite struct{}
|
||||
|
||||
func NewSysImInvite() *sSysImInvite { return &sSysImInvite{} }
|
||||
|
||||
func init() { service.RegisterSysImInvite(NewSysImInvite()) }
|
||||
|
||||
type imInviteListRequest struct {
|
||||
TenantID int64
|
||||
InviteCode string
|
||||
Status string
|
||||
BatchID int64
|
||||
CreatedBy int64
|
||||
CreatedAtStart *time.Time
|
||||
CreatedAtEnd *time.Time
|
||||
ExpiresAtStart *time.Time
|
||||
ExpiresAtEnd *time.Time
|
||||
}
|
||||
|
||||
type imInviteCodeResponse struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
BatchID *int64 `json:"batch_id"`
|
||||
Code string `json:"code"`
|
||||
Status string `json:"status"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
UsageCount int `json:"usage_count"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type imInviteBatchResponse struct {
|
||||
BatchID int64 `json:"batch_id"`
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
Quantity int `json:"quantity"`
|
||||
IdempotencyKey string `json:"idempotency_key"`
|
||||
Remark string `json:"remark"`
|
||||
ActiveCount int `json:"active_count"`
|
||||
UsageCount int `json:"usage_count"`
|
||||
RevokedCount int `json:"revoked_count"`
|
||||
ExpiredCount int `json:"expired_count"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
scope, db, err := inviteQueryDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req, err := buildInviteListRequest(in)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !validInviteStatus(req.Status) {
|
||||
return nil, 0, gerror.New("邀请码状态不正确")
|
||||
}
|
||||
req.TenantID = scope.TenantId
|
||||
items, total, err := queryInviteCodes(ctx, db, req, in.PerPage, (in.Page-1)*in.PerPage)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
} else {
|
||||
list = convertInviteCodes(items)
|
||||
totalCount = total
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_list", "invite_code", 0, result, g.Map{"hasInviteCode": in.InviteCode != ""})
|
||||
return list, totalCount, err
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expiresAt := in.ExpiresAt.Time.UTC().Round(time.Millisecond)
|
||||
var item imInviteCodeResponse
|
||||
if key := strings.TrimSpace(in.IdempotencyKey); key != "" {
|
||||
batch, codes, createErr := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, 1, expiresAt, in.Remark, key)
|
||||
if createErr != nil {
|
||||
err = createErr
|
||||
} else if len(codes) > 0 {
|
||||
item = codes[0]
|
||||
} else {
|
||||
codes, _, err = queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: batch.BatchID}, 1, 0)
|
||||
if err == nil && len(codes) > 0 {
|
||||
item = codes[0]
|
||||
} else if err == nil {
|
||||
err = gerror.New("邀请码生成结果不存在")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item, err = createSingleInvite(ctx, db, scope.TenantId, scope.OperatorId, expiresAt, in.Remark)
|
||||
}
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
} else {
|
||||
res = convertInviteCode(item)
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_create", "invite_code", item.ID, result, g.Map{"expiresAt": in.ExpiresAt, "remark": in.Remark})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := strings.TrimSpace(in.IdempotencyKey)
|
||||
batch, _, err := createInviteBatch(ctx, db, scope.TenantId, scope.OperatorId, in.Quantity, in.ExpiresAt.Time.UTC().Round(time.Millisecond), in.Remark, key)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
} else {
|
||||
res = convertInviteBatch(batch)
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_create", "invite_batch", batchID(batch), result, g.Map{"quantity": in.Quantity, "expiresAt": in.ExpiresAt, "remark": in.Remark})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error) {
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
batch, err := getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
codes, _, err := queryInviteCodes(ctx, db, imInviteListRequest{TenantID: scope.TenantId, BatchID: id}, batch.Quantity, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sysin.ImInviteBatchDetailModel{ImInviteBatchModel: *convertInviteBatch(batch), List: convertInviteCodes(codes)}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error) {
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := sysin.ParseOptionalInt64(in.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
result, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=?
|
||||
WHERE tenant_id=? AND id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now)
|
||||
if err == nil {
|
||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||
err = gerror.New("有效邀请码不存在")
|
||||
}
|
||||
}
|
||||
auditResult := "success"
|
||||
if err != nil {
|
||||
auditResult = "failed"
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_revoke", "invite_code", id, auditResult, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error) {
|
||||
scope, db, err := inviteDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id); err == nil {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
_, err = db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='revoked',updated_at=?
|
||||
WHERE tenant_id=? AND batch_id=? AND status='active' AND expires_at>?`, now, scope.TenantId, id, now)
|
||||
}
|
||||
var batch *imInviteBatchResponse
|
||||
if err == nil {
|
||||
batch, err = getInviteBatch(ctx, db, scope.TenantId, "b.id=?", id)
|
||||
}
|
||||
auditResult := "success"
|
||||
if err != nil {
|
||||
auditResult = "failed"
|
||||
} else {
|
||||
res = convertInviteBatch(batch)
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_batch_revoke", "invite_batch", id, auditResult, nil)
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *sSysImInvite) Export(ctx context.Context, in *sysin.ImInviteListInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
scope, db, err := inviteQueryDB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := buildInviteListRequest(in)
|
||||
if err != nil || !validInviteStatus(req.Status) {
|
||||
return gerror.New("导出条件不正确")
|
||||
}
|
||||
req.TenantID = scope.TenantId
|
||||
request := ghttp.RequestFromCtx(ctx)
|
||||
if request == nil {
|
||||
return gerror.New("请求上下文不存在")
|
||||
}
|
||||
request.Response.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
||||
request.Response.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=registration-invite-codes-%s.csv", time.Now().Format("20060102-150405")))
|
||||
_, _ = request.Response.Writer.Write([]byte{0xef, 0xbb, 0xbf})
|
||||
writer := csv.NewWriter(request.Response.Writer)
|
||||
_ = writer.Write([]string{"tenant_id", "tenant_name", "code", "status", "batch_id", "expires_at", "usage_count", "last_used_at", "created_by", "created_at", "remark"})
|
||||
for offset := 0; ; offset += 100 {
|
||||
items, total, queryErr := queryInviteCodes(ctx, db, req, 100, offset)
|
||||
if queryErr != nil {
|
||||
return gerror.Wrap(queryErr, "导出邀请码失败")
|
||||
}
|
||||
for _, item := range items {
|
||||
_ = writer.Write([]string{strconv.FormatInt(item.TenantID, 10), item.TenantName, item.Code, item.Status, formatOptionalInt64(item.BatchID), item.ExpiresAt.Format(time.RFC3339), strconv.Itoa(item.UsageCount), formatTime(item.LastUsedAt), strconv.FormatInt(item.CreatedBy, 10), item.CreatedAt.Format(time.RFC3339), item.Remark})
|
||||
}
|
||||
writer.Flush()
|
||||
if writer.Error() != nil {
|
||||
return writer.Error()
|
||||
}
|
||||
if offset+len(items) >= total || len(items) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "invite_export", "invite_code", 0, "success", g.Map{"hasInviteCode": in.InviteCode != ""})
|
||||
return nil
|
||||
}
|
||||
|
||||
func inviteDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) {
|
||||
scope, err := resolveImAdminScope(ctx, selectedTenantID)
|
||||
if err != nil {
|
||||
return scope, nil, err
|
||||
}
|
||||
return scope, imtenant.RegistryDB(), nil
|
||||
}
|
||||
|
||||
func inviteQueryDB(ctx context.Context, selectedTenantID int64) (imAdminScope, gdb.DB, error) {
|
||||
scope, err := resolveImAdminScopeWithAll(ctx, selectedTenantID, true)
|
||||
if err != nil {
|
||||
return scope, nil, err
|
||||
}
|
||||
return scope, imtenant.RegistryDB(), nil
|
||||
}
|
||||
|
||||
func createSingleInvite(ctx context.Context, db gdb.DB, tenantID, operatorID int64, expiresAt time.Time, remark string) (imInviteCodeResponse, error) {
|
||||
for attempt := 0; attempt < inviteGenerateRetries; attempt++ {
|
||||
id, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return imInviteCodeResponse{}, err
|
||||
}
|
||||
code, err := randomInviteCode()
|
||||
if err != nil {
|
||||
return imInviteCodeResponse{}, err
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
_, err = db.Exec(ctx, `INSERT INTO im_registration_invite_codes
|
||||
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
|
||||
VALUES(?,?,NULL,?,'active',?,0,NULL,?,?,?,?)`, id, tenantID, code, expiresAt, operatorID, remark, now, now)
|
||||
if err == nil {
|
||||
return imInviteCodeResponse{ID: id, TenantID: tenantID, Code: code, Status: "active", ExpiresAt: expiresAt, CreatedBy: operatorID, Remark: remark, CreatedAt: now}, nil
|
||||
}
|
||||
if !isDuplicateError(err) {
|
||||
return imInviteCodeResponse{}, err
|
||||
}
|
||||
}
|
||||
return imInviteCodeResponse{}, gerror.New("无法生成不重复的邀请码,请重试")
|
||||
}
|
||||
|
||||
func createInviteBatch(ctx context.Context, db gdb.DB, tenantID, operatorID int64, quantity int, expiresAt time.Time, remark, key string) (*imInviteBatchResponse, []imInviteCodeResponse, error) {
|
||||
if existing, err := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); err == nil {
|
||||
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
||||
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
||||
}
|
||||
return existing, nil, nil
|
||||
} else if !errors.Is(err, errInviteBatchNotFound) {
|
||||
return nil, nil, err
|
||||
}
|
||||
for attempt := 0; attempt < inviteGenerateRetries; attempt++ {
|
||||
batchID, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
codes := make([]imInviteCodeResponse, quantity)
|
||||
seen := make(map[string]struct{}, quantity)
|
||||
for i := range codes {
|
||||
for {
|
||||
code, randomErr := randomInviteCode()
|
||||
if randomErr != nil {
|
||||
return nil, nil, randomErr
|
||||
}
|
||||
if _, ok := seen[code]; ok {
|
||||
continue
|
||||
}
|
||||
seen[code] = struct{}{}
|
||||
codes[i].Code = code
|
||||
break
|
||||
}
|
||||
codes[i].ID, err = imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
codes[i].BatchID = &batchID
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_batches
|
||||
(id,tenant_id,quantity,idempotency_key,expires_at,created_by,remark,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?)`, batchID, tenantID, quantity, key, expiresAt, operatorID, remark, now, now); txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
for i := range codes {
|
||||
if _, txErr := tx.Exec(`INSERT INTO im_registration_invite_codes
|
||||
(id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at)
|
||||
VALUES(?,?,?,?,'active',?,0,NULL,?,?,?,?)`, codes[i].ID, tenantID, batchID, codes[i].Code, expiresAt, operatorID, remark, now, now); txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
codes[i].TenantID, codes[i].Status, codes[i].ExpiresAt, codes[i].CreatedBy, codes[i].Remark, codes[i].CreatedAt = tenantID, "active", expiresAt, operatorID, remark, now
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
return &imInviteBatchResponse{BatchID: batchID, TenantID: tenantID, Quantity: quantity, IdempotencyKey: key, Remark: remark, ActiveCount: quantity, ExpiresAt: expiresAt, CreatedAt: now}, codes, nil
|
||||
}
|
||||
if !isDuplicateError(err) {
|
||||
return nil, nil, err
|
||||
}
|
||||
if existing, lookupErr := getInviteBatch(ctx, db, tenantID, "b.idempotency_key=?", key); lookupErr == nil {
|
||||
if !sameInviteBatch(existing, quantity, expiresAt, remark) {
|
||||
return nil, nil, gerror.New("幂等键已被其他批量生成请求使用")
|
||||
}
|
||||
return existing, nil, nil
|
||||
} else if !errors.Is(lookupErr, errInviteBatchNotFound) {
|
||||
return nil, nil, lookupErr
|
||||
}
|
||||
}
|
||||
return nil, nil, gerror.New("无法生成不重复的邀请码,请重试")
|
||||
}
|
||||
|
||||
func queryInviteCodes(ctx context.Context, db gdb.DB, req imInviteListRequest, limit, offset int) ([]imInviteCodeResponse, int, error) {
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
expireWhere := "status='active' AND expires_at<=?"
|
||||
expireArgs := []any{now, now}
|
||||
if req.TenantID > 0 {
|
||||
expireWhere = "tenant_id=? AND " + expireWhere
|
||||
expireArgs = []any{now, req.TenantID, now}
|
||||
}
|
||||
if _, err := db.Exec(ctx, `UPDATE im_registration_invite_codes SET status='expired',updated_at=? WHERE `+expireWhere, expireArgs...); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
where, args := inviteWhere(req)
|
||||
totalValue, err := db.GetValue(ctx, "SELECT COUNT(*) FROM im_registration_invite_codes c WHERE "+where, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
queryArgs := append(append([]any{}, args...), limit, offset)
|
||||
items := make([]imInviteCodeResponse, 0)
|
||||
err = db.Raw(`SELECT c.id,c.tenant_id,t.name tenant_name,c.batch_id,c.code,c.status,c.expires_at,c.created_by,c.remark,c.created_at,
|
||||
c.usage_count,c.last_used_at
|
||||
FROM im_registration_invite_codes c
|
||||
LEFT JOIN im_tenant t ON t.id=c.tenant_id
|
||||
WHERE `+where+` ORDER BY c.created_at DESC,c.id DESC LIMIT ? OFFSET ?`, queryArgs...).Ctx(ctx).Scan(&items)
|
||||
return items, totalValue.Int(), err
|
||||
}
|
||||
|
||||
func inviteWhere(req imInviteListRequest) (string, []any) {
|
||||
where := []string{"1=1"}
|
||||
args := make([]any, 0, 8)
|
||||
if req.TenantID > 0 {
|
||||
where, args = append(where, "c.tenant_id=?"), append(args, req.TenantID)
|
||||
}
|
||||
if req.InviteCode != "" {
|
||||
where, args = append(where, "c.code=?"), append(args, req.InviteCode)
|
||||
}
|
||||
if req.Status != "" {
|
||||
where, args = append(where, "c.status=?"), append(args, req.Status)
|
||||
}
|
||||
if req.BatchID > 0 {
|
||||
where, args = append(where, "c.batch_id=?"), append(args, req.BatchID)
|
||||
}
|
||||
if req.CreatedBy > 0 {
|
||||
where, args = append(where, "c.created_by=?"), append(args, req.CreatedBy)
|
||||
}
|
||||
if req.CreatedAtStart != nil {
|
||||
where, args = append(where, "c.created_at>=?"), append(args, *req.CreatedAtStart)
|
||||
}
|
||||
if req.CreatedAtEnd != nil {
|
||||
where, args = append(where, "c.created_at<=?"), append(args, *req.CreatedAtEnd)
|
||||
}
|
||||
if req.ExpiresAtStart != nil {
|
||||
where, args = append(where, "c.expires_at>=?"), append(args, *req.ExpiresAtStart)
|
||||
}
|
||||
if req.ExpiresAtEnd != nil {
|
||||
where, args = append(where, "c.expires_at<=?"), append(args, *req.ExpiresAtEnd)
|
||||
}
|
||||
return strings.Join(where, " AND "), args
|
||||
}
|
||||
|
||||
func getInviteBatch(ctx context.Context, db gdb.DB, tenantID int64, where string, arg any) (*imInviteBatchResponse, error) {
|
||||
items := make([]imInviteBatchResponse, 0, 1)
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
err := db.Raw(`SELECT b.id batch_id,b.tenant_id,b.quantity,b.idempotency_key,b.expires_at,b.remark,b.created_at,
|
||||
COALESCE(SUM(CASE WHEN c.status='active' AND c.expires_at>? THEN 1 ELSE 0 END),0) active_count,
|
||||
COALESCE(SUM(c.usage_count),0) usage_count,
|
||||
COALESCE(SUM(CASE WHEN (c.status='active' AND c.expires_at<=?) OR c.status='expired' THEN 1 ELSE 0 END),0) expired_count,
|
||||
COALESCE(SUM(CASE WHEN c.status='revoked' THEN 1 ELSE 0 END),0) revoked_count
|
||||
FROM im_registration_invite_batches b LEFT JOIN im_registration_invite_codes c ON c.tenant_id=b.tenant_id AND c.batch_id=b.id
|
||||
WHERE b.tenant_id=? AND `+where+` GROUP BY b.id LIMIT 1`, now, now, tenantID, arg).Ctx(ctx).Scan(&items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, gerror.Wrap(errInviteBatchNotFound, "邀请码批次不存在")
|
||||
}
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
func buildInviteListRequest(in *sysin.ImInviteListInp) (imInviteListRequest, error) {
|
||||
batchID, err := sysin.ParseOptionalInt64(in.BatchId)
|
||||
if err != nil {
|
||||
return imInviteListRequest{}, err
|
||||
}
|
||||
createdBy, err := sysin.ParseOptionalInt64(in.CreatedBy)
|
||||
if err != nil {
|
||||
return imInviteListRequest{}, err
|
||||
}
|
||||
return imInviteListRequest{InviteCode: in.InviteCode, Status: in.Status, BatchID: batchID, CreatedBy: createdBy,
|
||||
CreatedAtStart: nativeTime(in.CreatedAtStart), CreatedAtEnd: nativeTime(in.CreatedAtEnd),
|
||||
ExpiresAtStart: nativeTime(in.ExpiresAtStart), ExpiresAtEnd: nativeTime(in.ExpiresAtEnd)}, nil
|
||||
}
|
||||
|
||||
func randomInviteCode() (string, error) {
|
||||
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
result := make([]byte, 5)
|
||||
buffer := make([]byte, 10)
|
||||
for index := 0; index < len(result); {
|
||||
if _, err := cryptorand.Read(buffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, value := range buffer {
|
||||
if value >= 208 {
|
||||
continue
|
||||
}
|
||||
result[index] = alphabet[int(value)%len(alphabet)]
|
||||
index++
|
||||
if index == len(result) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
func isDuplicateError(err error) bool {
|
||||
var mysqlErr *mysqldriver.MySQLError
|
||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062
|
||||
}
|
||||
|
||||
func validInviteStatus(status string) bool {
|
||||
return status == "" || status == "active" || status == "expired" || status == "revoked"
|
||||
}
|
||||
|
||||
func sameInviteBatch(batch *imInviteBatchResponse, quantity int, expiresAt time.Time, remark string) bool {
|
||||
return batch != nil && batch.Quantity == quantity &&
|
||||
batch.ExpiresAt.UTC().Round(time.Millisecond).Equal(expiresAt.UTC().Round(time.Millisecond)) && batch.Remark == remark
|
||||
}
|
||||
|
||||
func convertInviteCodes(items []imInviteCodeResponse) []*sysin.ImInviteCodeModel {
|
||||
list := make([]*sysin.ImInviteCodeModel, 0, len(items))
|
||||
for _, item := range items {
|
||||
list = append(list, convertInviteCode(item))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func convertInviteCode(item imInviteCodeResponse) *sysin.ImInviteCodeModel {
|
||||
return &sysin.ImInviteCodeModel{Id: strconv.FormatInt(item.ID, 10), BatchId: formatOptionalInt64(item.BatchID), Code: item.Code,
|
||||
TenantId: strconv.FormatInt(item.TenantID, 10), TenantName: item.TenantName,
|
||||
Status: item.Status, ExpiresAt: gtime.New(item.ExpiresAt), UsageCount: item.UsageCount, LastUsedAt: convertTime(item.LastUsedAt),
|
||||
CreatedBy: strconv.FormatInt(item.CreatedBy, 10), Remark: item.Remark, CreatedAt: gtime.New(item.CreatedAt)}
|
||||
}
|
||||
|
||||
func convertInviteBatch(item *imInviteBatchResponse) *sysin.ImInviteBatchModel {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
return &sysin.ImInviteBatchModel{BatchId: strconv.FormatInt(item.BatchID, 10), Quantity: item.Quantity, ActiveCount: item.ActiveCount,
|
||||
TenantId: strconv.FormatInt(item.TenantID, 10),
|
||||
UsageCount: item.UsageCount, RevokedCount: item.RevokedCount, ExpiredCount: item.ExpiredCount,
|
||||
ExpiresAt: gtime.New(item.ExpiresAt), CreatedAt: gtime.New(item.CreatedAt)}
|
||||
}
|
||||
|
||||
func nativeTime(value *gtime.Time) *time.Time {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
t := value.Time.UTC()
|
||||
return &t
|
||||
}
|
||||
|
||||
func convertTime(value *time.Time) *gtime.Time {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return gtime.New(value)
|
||||
}
|
||||
|
||||
func formatOptionalInt64(value *int64) string {
|
||||
if value == nil || *value == 0 {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(*value, 10)
|
||||
}
|
||||
|
||||
func formatTime(value *time.Time) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return value.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func batchID(batch *imInviteBatchResponse) int64 {
|
||||
if batch == nil {
|
||||
return 0
|
||||
}
|
||||
return batch.BatchID
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hotgo/internal/library/imtenant"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestRandomInviteCode(t *testing.T) {
|
||||
pattern := regexp.MustCompile(`^[A-Za-z]{5}$`)
|
||||
for i := 0; i < 100; i++ {
|
||||
code, err := randomInviteCode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !pattern.MatchString(code) {
|
||||
t.Fatalf("invalid invite code %q", code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteDirectDatabase(t *testing.T) {
|
||||
link := os.Getenv("IM_MANAGER_INTEGRATION_DSN")
|
||||
if link == "" {
|
||||
t.Skip("IM_MANAGER_INTEGRATION_DSN is not set")
|
||||
}
|
||||
const group = "im_invite_integration"
|
||||
if err := gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := gdb.Instance(group)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
key := "integration-" + time.Now().UTC().Format("20060102150405.000000000")
|
||||
expiresAt := time.Now().UTC().Add(time.Hour).Round(time.Millisecond)
|
||||
batch, codes, err := createInviteBatch(ctx, db, 1, 1, 3, expiresAt, "integration", key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_registration_invite_codes WHERE tenant_id=? AND batch_id=?", 1, batch.BatchID)
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_registration_invite_batches WHERE tenant_id=? AND id=?", 1, batch.BatchID)
|
||||
})
|
||||
if batch.ActiveCount != 3 || len(codes) != 3 {
|
||||
t.Fatalf("unexpected batch result: %#v codes=%d", batch, len(codes))
|
||||
}
|
||||
replayed, replayCodes, err := createInviteBatch(ctx, db, 1, 1, 3, expiresAt, "integration", key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if replayed.BatchID != batch.BatchID || len(replayCodes) != 0 {
|
||||
t.Fatalf("idempotent replay created a new batch: %#v", replayed)
|
||||
}
|
||||
items, total, err := queryInviteCodes(ctx, db, imInviteListRequest{TenantID: 1, BatchID: batch.BatchID}, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if total != 3 || len(items) != 3 {
|
||||
t.Fatalf("unexpected query result total=%d items=%d", total, len(items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPasswordDirectDatabase(t *testing.T) {
|
||||
link := os.Getenv("IM_MANAGER_INTEGRATION_DSN")
|
||||
if link == "" {
|
||||
t.Skip("IM_MANAGER_INTEGRATION_DSN is not set")
|
||||
}
|
||||
const group = "im_password_reset_integration"
|
||||
if err := gdb.SetConfigGroup(group, gdb.ConfigGroup{{Link: link}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, err := gdb.Instance(group)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
userID, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
username := "integration" + time.Now().UTC().Format("150405000000")
|
||||
if _, err = db.Exec(ctx, `INSERT INTO im_users(id,username,createdat,updatedat,state,useragent)
|
||||
VALUES(?,?,?,?,0,'integration-test')`, userID, username, now, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = db.Exec(ctx, "INSERT INTO im_auth(userid,authlvl,secret) VALUES(?,20,'old')", userID); err != nil {
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_users WHERE id=?", userID)
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_business_events_outbox WHERE target=? AND event_type='security.password_reset'", userID)
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_user_security WHERE user_id=?", userID)
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_auth WHERE userid=?", userID)
|
||||
_, _ = db.Exec(ctx, "DELETE FROM im_users WHERE id=?", userID)
|
||||
})
|
||||
scope := imAdminScope{TenantId: 1, OperatorId: 99, RequestId: "integration-password-reset"}
|
||||
if err = resetIMUserPassword(ctx, db, scope, userID, "new-password"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret, err := db.GetValue(ctx, "SELECT secret FROM im_auth WHERE userid=?", userID)
|
||||
if err != nil || bcrypt.CompareHashAndPassword(secret.Bytes(), []byte("new-password")) != nil {
|
||||
t.Fatalf("password hash was not updated: %v", err)
|
||||
}
|
||||
version, err := db.GetValue(ctx, "SELECT auth_version FROM im_user_security WHERE user_id=?", userID)
|
||||
if err != nil || version.Int() != 2 {
|
||||
t.Fatalf("unexpected auth version %d: %v", version.Int(), err)
|
||||
}
|
||||
events, err := db.GetValue(ctx, `SELECT COUNT(*) FROM im_business_events_outbox
|
||||
WHERE target=? AND event_type='security.password_reset' AND status='pending'`, userID)
|
||||
if err != nil || events.Int() != 1 {
|
||||
t.Fatalf("password reset event not written: count=%d err=%v", events.Int(), err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sSysImRealname struct{}
|
||||
|
||||
func NewSysImRealname() *sSysImRealname {
|
||||
return &sSysImRealname{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImRealname(NewSysImRealname())
|
||||
}
|
||||
|
||||
// List 获取IM实名审核列表
|
||||
func (s *sSysImRealname) List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.model(ctx, db)
|
||||
mod = s.applyWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("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
|
||||
}
|
||||
|
||||
func (s *sSysImRealname) model(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model("im_realname_verifications").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"user_id",
|
||||
"submission_id",
|
||||
"status",
|
||||
"id_type",
|
||||
"masked_name",
|
||||
"masked_id_number",
|
||||
"review_reason",
|
||||
"verified_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImRealname) applyWhere(mod *gdb.Model, in *sysin.ImRealnameListInp) *gdb.Model {
|
||||
if in.UserId > 0 {
|
||||
mod = mod.Where("user_id", in.UserId)
|
||||
}
|
||||
if in.SubmissionId != "" {
|
||||
mod = mod.WhereLike("submission_id", "%"+in.SubmissionId+"%")
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// View 获取IM实名审核指定信息
|
||||
func (s *sSysImRealname) View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = db.Model("im_realname_verifications").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"id",
|
||||
"user_id",
|
||||
"submission_id",
|
||||
"status",
|
||||
"id_type",
|
||||
"masked_name",
|
||||
"masked_id_number",
|
||||
"id_number_hash",
|
||||
"review_reason",
|
||||
"verified_at",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
Where("id", in.Id).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM实名审核信息失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
return
|
||||
}
|
||||
|
||||
// Review 审核IM实名
|
||||
func (s *sSysImRealname) Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
"status": in.Status,
|
||||
"review_reason": in.ReviewReason,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
if in.Status == consts.ImRealnameStatusVerified {
|
||||
data["verified_at"] = gtime.Now()
|
||||
} else {
|
||||
data["verified_at"] = nil
|
||||
}
|
||||
|
||||
result, err := db.Model("im_realname_verifications").Safe().Ctx(ctx).
|
||||
Where("id", in.Id).
|
||||
Where("status", consts.ImRealnameStatusPending).
|
||||
Data(data).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "审核IM实名失败,请稍后重试!")
|
||||
}
|
||||
affected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取审核结果失败,请稍后重试!")
|
||||
}
|
||||
if affected == 0 {
|
||||
return gerror.New("当前记录不是待审核状态,不能重复审核")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
imRegistrationConfigGroup = "im"
|
||||
imRegistrationSmsKey = "register_sms_enabled"
|
||||
)
|
||||
|
||||
type sSysImRegistration struct{}
|
||||
|
||||
func NewSysImRegistration() *sSysImRegistration {
|
||||
return &sSysImRegistration{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImRegistration(NewSysImRegistration())
|
||||
}
|
||||
|
||||
func (s *sSysImRegistration) GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) {
|
||||
if err = s.verifySuper(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
return s.getConfig(ctx)
|
||||
}
|
||||
|
||||
func (s *sSysImRegistration) UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error) {
|
||||
if err = s.verifySuper(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
old, err := s.getConfig(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value := "0"
|
||||
if in.RegisterSmsEnabled {
|
||||
value = "1"
|
||||
}
|
||||
_, err = dao.SysConfig.Ctx(ctx).
|
||||
Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup).
|
||||
Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey).
|
||||
Data(g.Map{
|
||||
dao.SysConfig.Columns().Value: value,
|
||||
dao.SysConfig.Columns().UpdatedAt: gtime.Now(),
|
||||
}).
|
||||
Update()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "更新IM注册短信开关失败")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, 0, "registration_sms_config_update", "config", 0, "success", g.Map{
|
||||
"old": old.RegisterSmsEnabled,
|
||||
"new": in.RegisterSmsEnabled,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImRegistration) getConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error) {
|
||||
var record gdb.Record
|
||||
record, err = dao.SysConfig.Ctx(ctx).
|
||||
Where(dao.SysConfig.Columns().Group, imRegistrationConfigGroup).
|
||||
Where(dao.SysConfig.Columns().Key, imRegistrationSmsKey).
|
||||
Where(dao.SysConfig.Columns().Status, 1).
|
||||
One()
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM注册短信开关失败")
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
if err = s.seedConfig(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.getConfig(ctx)
|
||||
}
|
||||
return &sysin.ImRegistrationConfigModel{
|
||||
RegisterSmsEnabled: record[dao.SysConfig.Columns().Value].String() == "1",
|
||||
UpdatedAt: record[dao.SysConfig.Columns().UpdatedAt].GTime(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sSysImRegistration) seedConfig(ctx context.Context) (err error) {
|
||||
_, err = dao.SysConfig.Ctx(ctx).Data(g.Map{
|
||||
dao.SysConfig.Columns().Group: imRegistrationConfigGroup,
|
||||
dao.SysConfig.Columns().Name: "注册短信发送",
|
||||
dao.SysConfig.Columns().Type: "bool",
|
||||
dao.SysConfig.Columns().Key: imRegistrationSmsKey,
|
||||
dao.SysConfig.Columns().Value: "1",
|
||||
dao.SysConfig.Columns().DefaultValue: "1",
|
||||
dao.SysConfig.Columns().Sort: 10,
|
||||
dao.SysConfig.Columns().Tip: "控制 IM 注册验证码是否真实发送",
|
||||
dao.SysConfig.Columns().IsDefault: 1,
|
||||
dao.SysConfig.Columns().Status: 1,
|
||||
dao.SysConfig.Columns().CreatedAt: gtime.Now(),
|
||||
dao.SysConfig.Columns().UpdatedAt: gtime.Now(),
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "初始化IM注册短信开关失败")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImRegistration) verifySuper(ctx context.Context) error {
|
||||
if !service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx)) {
|
||||
return gerror.New("仅平台超级管理员可操作IM注册配置")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/dao"
|
||||
"hotgo/internal/library/contexts"
|
||||
"hotgo/internal/library/hgorm/handler"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
)
|
||||
|
||||
type sSysImTenant struct{}
|
||||
|
||||
const imRegistryDbGroup = "imRegistry"
|
||||
|
||||
func NewSysImTenant() *sSysImTenant {
|
||||
return &sSysImTenant{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImTenant(NewSysImTenant())
|
||||
}
|
||||
|
||||
// Model IM租户ORM模型
|
||||
func (s *sSysImTenant) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
|
||||
return handler.Model(g.DB(imRegistryDbGroup).Model(dao.ImTenant.Table()).Safe().Ctx(ctx), option...)
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) configModel(ctx context.Context) *gdb.Model {
|
||||
return g.DB(imRegistryDbGroup).Model(dao.ImTenantConfig.Table()).Safe().Ctx(ctx)
|
||||
}
|
||||
|
||||
// List 获取IM租户列表
|
||||
func (s *sSysImTenant) List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.Model(ctx).Fields(
|
||||
dao.ImTenant.Columns().Id,
|
||||
dao.ImTenant.Columns().Code,
|
||||
dao.ImTenant.Columns().Name,
|
||||
dao.ImTenant.Columns().TenantDesc,
|
||||
dao.ImTenant.Columns().DbName,
|
||||
dao.ImTenant.Columns().State,
|
||||
dao.ImTenant.Columns().CreatedBy,
|
||||
dao.ImTenant.Columns().UpdatedBy,
|
||||
dao.ImTenant.Columns().CreatedAt,
|
||||
dao.ImTenant.Columns().UpdatedAt,
|
||||
)
|
||||
|
||||
if in.Id > 0 {
|
||||
mod = mod.Where(dao.ImTenant.Columns().Id, in.Id)
|
||||
}
|
||||
if in.Code != "" {
|
||||
mod = mod.WhereLike(dao.ImTenant.Columns().Code, "%"+in.Code+"%")
|
||||
}
|
||||
if in.Name != "" {
|
||||
mod = mod.WhereLike(dao.ImTenant.Columns().Name, "%"+in.Name+"%")
|
||||
}
|
||||
if in.DbName != "" {
|
||||
mod = mod.WhereLike(dao.ImTenant.Columns().DbName, "%"+in.DbName+"%")
|
||||
}
|
||||
if in.State != nil {
|
||||
mod = mod.Where(dao.ImTenant.Columns().State, *in.State)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween(dao.ImTenant.Columns().CreatedAt, in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
|
||||
mod = mod.Page(in.Page, in.PerPage).
|
||||
OrderDesc(dao.ImTenant.Columns().Id).
|
||||
Hook(hook.MemberSummary)
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM租户列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.fillListConfig(ctx, list); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) fillListConfig(ctx context.Context, list []*sysin.ImTenantListModel) (err error) {
|
||||
ids := make([]int64, 0, len(list))
|
||||
for _, item := range list {
|
||||
ids = append(ids, item.Id)
|
||||
}
|
||||
|
||||
var configs []*entity.ImTenantConfig
|
||||
if err = s.configModel(ctx).
|
||||
WhereIn(dao.ImTenantConfig.Columns().TenantId, ids).
|
||||
Scan(&configs); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
configMap := make(map[int64]*entity.ImTenantConfig, len(configs))
|
||||
for _, item := range configs {
|
||||
configMap[item.TenantId] = item
|
||||
}
|
||||
|
||||
var tenants []*entity.ImTenant
|
||||
if err = s.Model(ctx).
|
||||
Fields(dao.ImTenant.Columns().Id, dao.ImTenant.Columns().DbDsn).
|
||||
WhereIn(dao.ImTenant.Columns().Id, ids).
|
||||
Scan(&tenants); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户连接配置失败,请稍后重试!")
|
||||
}
|
||||
dsnMap := make(map[int64]string, len(tenants))
|
||||
for _, item := range tenants {
|
||||
dsnMap[item.Id] = item.DbDsn
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
if conf, ok := configMap[item.Id]; ok {
|
||||
item.MaxUsers = conf.MaxUsers
|
||||
item.MaxGroups = conf.MaxGroups
|
||||
item.MaxGroupMembers = conf.MaxGroupMembers
|
||||
}
|
||||
item.DbDsnConfigured = dsnMap[item.Id] != ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Edit 修改/新增IM租户
|
||||
func (s *sSysImTenant) Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return g.DB(imRegistryDbGroup).Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if err = s.checkCodeUnique(ctx, in.Id, in.Code); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userId := contexts.GetUserId(ctx)
|
||||
if in.Id > 0 {
|
||||
in.UpdatedBy = userId
|
||||
if _, err = s.Model(ctx).
|
||||
Fields(sysin.ImTenantUpdateFields{}).
|
||||
WherePri(in.Id).Data(in).Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改IM租户失败,请稍后重试!")
|
||||
}
|
||||
return s.saveConfig(ctx, in.Id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
|
||||
}
|
||||
|
||||
in.CreatedBy = userId
|
||||
id, err := s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
Fields(sysin.ImTenantInsertFields{}).
|
||||
Data(in).InsertAndGetId()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
||||
}
|
||||
return s.saveConfig(ctx, id, in.MaxUsers, in.MaxGroups, in.MaxGroupMembers, userId)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
|
||||
mod := s.Model(ctx).Where(dao.ImTenant.Columns().Code, code)
|
||||
if id > 0 {
|
||||
mod = mod.Where(dao.ImTenant.Columns().Id+"<>?", id)
|
||||
}
|
||||
count, err := mod.Count()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "检查IM租户编码失败,请稍后重试!")
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("租户编码已存在")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) saveConfig(ctx context.Context, tenantId int64, maxUsers uint64, maxGroups uint64, maxGroupMembers uint, userId int64) (err error) {
|
||||
var conf *entity.ImTenantConfig
|
||||
if err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, tenantId).
|
||||
Scan(&conf); err != nil {
|
||||
return gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
|
||||
data := g.Map{
|
||||
dao.ImTenantConfig.Columns().MaxUsers: maxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups: maxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers: maxGroupMembers,
|
||||
}
|
||||
if conf != nil && conf.Id > 0 {
|
||||
data[dao.ImTenantConfig.Columns().UpdatedBy] = userId
|
||||
if _, err = s.configModel(ctx).
|
||||
Fields(
|
||||
dao.ImTenantConfig.Columns().MaxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||
dao.ImTenantConfig.Columns().UpdatedBy,
|
||||
).
|
||||
WherePri(conf.Id).Data(data).Update(); err != nil {
|
||||
return gerror.Wrap(err, "修改IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
data[dao.ImTenantConfig.Columns().TenantId] = tenantId
|
||||
data[dao.ImTenantConfig.Columns().CreatedBy] = userId
|
||||
if _, err = s.configModel(ctx).
|
||||
Fields(
|
||||
dao.ImTenantConfig.Columns().TenantId,
|
||||
dao.ImTenantConfig.Columns().MaxUsers,
|
||||
dao.ImTenantConfig.Columns().MaxGroups,
|
||||
dao.ImTenantConfig.Columns().MaxGroupMembers,
|
||||
dao.ImTenantConfig.Columns().CreatedBy,
|
||||
).
|
||||
Data(data).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "新增IM租户配置失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取IM租户指定信息
|
||||
func (s *sSysImTenant) View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if err = s.Model(ctx).WherePri(in.Id).Hook(hook.MemberSummary).Scan(&res); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM租户信息失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if res == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var conf *entity.ImTenantConfig
|
||||
if err = s.configModel(ctx).
|
||||
Where(dao.ImTenantConfig.Columns().TenantId, in.Id).
|
||||
Scan(&conf); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM租户配置失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if conf != nil {
|
||||
res.MaxUsers = conf.MaxUsers
|
||||
res.MaxGroups = conf.MaxGroups
|
||||
res.MaxGroupMembers = conf.MaxGroupMembers
|
||||
}
|
||||
res.DbDsnConfigured = res.DbDsn != ""
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新IM租户状态
|
||||
func (s *sSysImTenant) Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = s.Model(ctx).WherePri(in.Id).Data(g.Map{
|
||||
dao.ImTenant.Columns().State: in.State,
|
||||
dao.ImTenant.Columns().UpdatedBy: contexts.GetUserId(ctx),
|
||||
}).Update(); err != nil {
|
||||
err = gerror.Wrap(err, "更新IM租户状态失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type sSysImUser struct{}
|
||||
|
||||
func NewSysImUser() *sSysImUser {
|
||||
return &sSysImUser{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImUser(NewSysImUser())
|
||||
}
|
||||
|
||||
// List 获取IM用户列表
|
||||
func (s *sSysImUser) List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.listModel(ctx, db)
|
||||
mod = s.applyListWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("u.id")
|
||||
|
||||
if err = mod.ScanAndCount(&list, &totalCount, false); err != nil {
|
||||
err = gerror.Wrap(err, "获取IM用户列表失败,请稍后重试!")
|
||||
return
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
item.TenantId = tenant.Id
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImUser) listModel(ctx context.Context, db gdb.DB) *gdb.Model {
|
||||
return db.Model("im_users u").Safe().Ctx(ctx).
|
||||
LeftJoin("im_user_profiles up", "up.user_id = u.id").
|
||||
LeftJoin("im_user_security us", "us.user_id = u.id").
|
||||
Fields(
|
||||
"CAST(u.id AS CHAR) user_id",
|
||||
"u.username",
|
||||
"COALESCE(u.phone, '') phone",
|
||||
"u.state",
|
||||
"u.lastseen last_seen",
|
||||
"u.useragent user_agent",
|
||||
"u.createdat created_at",
|
||||
"u.updatedat updated_at",
|
||||
"up.nickname",
|
||||
"up.public_account",
|
||||
"up.gender",
|
||||
"up.region",
|
||||
"us.auth_version",
|
||||
"us.password_updated_at",
|
||||
)
|
||||
}
|
||||
|
||||
func (s *sSysImUser) applyListWhere(mod *gdb.Model, in *sysin.ImUserListInp) *gdb.Model {
|
||||
if userId, _ := sysin.ParseOptionalInt64(in.UserId); userId > 0 {
|
||||
mod = mod.Where("u.id", userId)
|
||||
}
|
||||
if in.Nickname != "" {
|
||||
mod = mod.WhereLike("up.nickname", "%"+in.Nickname+"%")
|
||||
}
|
||||
if in.AuthName != "" {
|
||||
mod = mod.Where("u.username = ? OR u.phone = ? OR up.email = ?", in.AuthName, in.AuthName, in.AuthName)
|
||||
}
|
||||
if in.State != nil {
|
||||
mod = mod.Where("u.state", *in.State)
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("u.createdat", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
if len(in.LastSeen) == 2 {
|
||||
mod = mod.WhereBetween("u.lastseen", in.LastSeen[0], in.LastSeen[1])
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
func (s *sSysImUser) fillAuthAndCredentials(ctx context.Context, db gdb.DB, list []*sysin.ImUserListModel) (err error) {
|
||||
ids := make([]int64, 0, len(list))
|
||||
userMap := make(map[int64]*sysin.ImUserListModel, len(list))
|
||||
for _, item := range list {
|
||||
userId, parseErr := sysin.ParseOptionalInt64(item.UserId)
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
ids = append(ids, userId)
|
||||
userMap[userId] = item
|
||||
}
|
||||
|
||||
credentialRows, err := db.Model("im_credentials").Safe().Ctx(ctx).
|
||||
Fields("userid", "method", "value", "done").
|
||||
WhereIn("userid", ids).
|
||||
WhereNull("deletedat").
|
||||
OrderAsc("method").OrderAsc("value").
|
||||
All()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "获取IM用户凭证失败,请稍后重试!")
|
||||
}
|
||||
credentialMap := make(map[int64][]string)
|
||||
for _, row := range credentialRows {
|
||||
userId := row["userid"].Int64()
|
||||
method := row["method"].String()
|
||||
value := row["value"].String()
|
||||
done := row["done"].Bool()
|
||||
if !done {
|
||||
value += "(未验证)"
|
||||
}
|
||||
credentialMap[userId] = append(credentialMap[userId], method+":"+value)
|
||||
}
|
||||
|
||||
for userId, item := range userMap {
|
||||
item.AuthNames = item.Username
|
||||
if item.Phone != "" && item.Phone != item.Username {
|
||||
item.AuthNames += ", " + item.Phone
|
||||
}
|
||||
item.Credentials = strings.Join(credentialMap[userId], ", ")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// View 获取IM用户指定信息
|
||||
func (s *sSysImUser) View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userId, err := sysin.ParseOptionalInt64(in.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fields := append(s.viewFields(), "u.access access_json", "u.public profile_json", "u.trusted trusted_json")
|
||||
err = db.Model("im_users u").Safe().Ctx(ctx).
|
||||
LeftJoin("im_user_profiles up", "up.user_id = u.id").
|
||||
LeftJoin("im_user_security us", "us.user_id = u.id").
|
||||
Fields(fields...).
|
||||
Where("u.id", userId).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM用户信息失败,请稍后重试!")
|
||||
}
|
||||
if res == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
list := []*sysin.ImUserListModel{&res.ImUserListModel}
|
||||
if err = s.fillAuthAndCredentials(ctx, db, list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImUser) viewFields() []interface{} {
|
||||
return []interface{}{
|
||||
"CAST(u.id AS CHAR) user_id",
|
||||
"u.username",
|
||||
"COALESCE(u.phone, '') phone",
|
||||
"u.state",
|
||||
"u.lastseen last_seen",
|
||||
"u.useragent user_agent",
|
||||
"u.createdat created_at",
|
||||
"u.updatedat updated_at",
|
||||
"up.nickname",
|
||||
"up.public_account",
|
||||
"up.gender",
|
||||
"up.region",
|
||||
"up.signature",
|
||||
"up.avatar_file_id",
|
||||
"CAST(up.birth_date AS CHAR) birth_date",
|
||||
"up.version profile_version",
|
||||
"us.auth_version",
|
||||
"us.password_updated_at",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sSysImUser) ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
scope, err := resolveImAdminScope(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userId, err := sysin.ParseOptionalInt64(in.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, scope.TenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = resetIMUserPassword(ctx, db, scope, userId, in.NewPassword)
|
||||
result := "success"
|
||||
if err != nil {
|
||||
result = "failed"
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, scope.TenantId, "user_password_reset", "user", userId, result, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func resetIMUserPassword(ctx context.Context, db gdb.DB, scope imAdminScope, userId int64, newPassword string) error {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成密码摘要失败")
|
||||
}
|
||||
securityId, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成安全记录ID失败")
|
||||
}
|
||||
eventId, err := imtenant.NextSnowflakeID(ctx)
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "生成安全事件ID失败")
|
||||
}
|
||||
now := time.Now().UTC().Round(time.Millisecond)
|
||||
requestId := scope.RequestId
|
||||
if requestId == "" {
|
||||
requestId = gctx.CtxId(ctx)
|
||||
}
|
||||
if requestId == "" {
|
||||
requestId = guid.S()
|
||||
}
|
||||
eventKeyBytes := sha256.Sum256([]byte(fmt.Sprintf("password-reset:%d:%d:%s", scope.TenantId, userId, requestId)))
|
||||
eventKey := hex.EncodeToString(eventKeyBytes[:])
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"user_id": userId, "operator_id": scope.OperatorId, "request_id": requestId,
|
||||
})
|
||||
return db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
exists, txErr := tx.GetValue("SELECT EXISTS(SELECT 1 FROM im_users WHERE id=? LIMIT 1)", userId)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
if !exists.Bool() {
|
||||
return gerror.New("IM用户不存在")
|
||||
}
|
||||
result, txErr := tx.Exec("UPDATE im_auth SET secret=? WHERE userid=?", passwordHash, userId)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
if affected, _ := result.RowsAffected(); affected == 0 {
|
||||
return gerror.New("用户认证记录不存在")
|
||||
}
|
||||
if _, txErr = tx.Exec(`INSERT INTO im_user_security
|
||||
(id,user_id,auth_version,password_updated_at,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?)
|
||||
ON DUPLICATE KEY UPDATE auth_version=auth_version+1,
|
||||
password_updated_at=VALUES(password_updated_at),updated_at=VALUES(updated_at)`,
|
||||
securityId, userId, 2, now, now, now); txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
version, txErr := tx.GetValue("SELECT auth_version FROM im_user_security WHERE user_id=?", userId)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
var eventPayload map[string]any
|
||||
_ = json.Unmarshal(payload, &eventPayload)
|
||||
eventPayload["auth_version"] = version.Int()
|
||||
payload, _ = json.Marshal(eventPayload)
|
||||
_, txErr = tx.Exec(`INSERT INTO im_business_events_outbox
|
||||
(id,event_id,target_type,target,actor_user_id,event_type,payload,status,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,CAST(? AS JSON),'pending',?,?)
|
||||
ON DUPLICATE KEY UPDATE event_id=VALUES(event_id)`, eventId, eventKey, "user",
|
||||
fmt.Sprintf("%d", userId), 0, "security.password_reset", string(payload), now, now)
|
||||
return txErr
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// Package sys
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sys
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sSysImWorkOrder struct{}
|
||||
|
||||
func NewSysImWorkOrder() *sSysImWorkOrder {
|
||||
return &sSysImWorkOrder{}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterSysImWorkOrder(NewSysImWorkOrder())
|
||||
}
|
||||
|
||||
// List 获取IM举报反馈工单列表
|
||||
func (s *sSysImWorkOrder) List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mod := s.listModel(ctx, db, in.Type)
|
||||
mod = s.applyWhere(mod, in)
|
||||
mod = mod.Page(in.Page, in.PerPage).OrderDesc("created_at").OrderDesc("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
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) listModel(ctx context.Context, db gdb.DB, typ string) *gdb.Model {
|
||||
if typ == sysin.ImWorkOrderTypeAbuse {
|
||||
return db.Model("im_abuse_reports").Safe().Ctx(ctx).Fields(s.abuseListFields()...)
|
||||
}
|
||||
return db.Model("im_feedback_submissions").Safe().Ctx(ctx).Fields(s.feedbackListFields()...)
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) abuseListFields() []interface{} {
|
||||
return []interface{}{
|
||||
"'abuse' type",
|
||||
"id",
|
||||
"report_id order_id",
|
||||
"reporter_id user_id",
|
||||
"target_type",
|
||||
"CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary",
|
||||
"reason_code category",
|
||||
"description content",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) feedbackListFields() []interface{} {
|
||||
return []interface{}{
|
||||
"'feedback' type",
|
||||
"id",
|
||||
"feedback_id order_id",
|
||||
"user_id",
|
||||
"'' target_type",
|
||||
"'' target_summary",
|
||||
"category",
|
||||
"content",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) applyWhere(mod *gdb.Model, in *sysin.ImWorkOrderListInp) *gdb.Model {
|
||||
if in.UserId > 0 {
|
||||
mod = mod.Where("user_id", in.UserId)
|
||||
}
|
||||
if in.Status != "" {
|
||||
mod = mod.Where("status", in.Status)
|
||||
}
|
||||
if in.Keyword != "" {
|
||||
mod = mod.WhereLike("content", "%"+in.Keyword+"%")
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
mod = mod.WhereBetween("created_at", in.CreatedAt[0], in.CreatedAt[1])
|
||||
}
|
||||
return mod
|
||||
}
|
||||
|
||||
// View 获取IM举报反馈工单指定信息
|
||||
func (s *sSysImWorkOrder) View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, tenant, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if in.Type == sysin.ImWorkOrderTypeAbuse {
|
||||
res, err = s.viewAbuse(ctx, db, in.Id)
|
||||
} else {
|
||||
res, err = s.viewFeedback(ctx, db, in.Id)
|
||||
}
|
||||
if err != nil || res == nil {
|
||||
return
|
||||
}
|
||||
res.TenantId = tenant.Id
|
||||
res.TenantName = tenant.Name
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) viewAbuse(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) {
|
||||
err = db.Model("im_abuse_reports").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"'abuse' type",
|
||||
"id",
|
||||
"report_id order_id",
|
||||
"reporter_id user_id",
|
||||
"target_type",
|
||||
"CONCAT(target_topic, IF(target_seq>0, CONCAT('#', target_seq), ''), IF(target_user_id>0, CONCAT(' user:', target_user_id), '')) target_summary",
|
||||
"reason_code category",
|
||||
"description content",
|
||||
"description",
|
||||
"evidence_file_ids evidence_json",
|
||||
"snapshot snapshot_json",
|
||||
"idempotency_key",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
Where("id", id).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM举报工单信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *sSysImWorkOrder) viewFeedback(ctx context.Context, db gdb.DB, id int64) (res *sysin.ImWorkOrderViewModel, err error) {
|
||||
err = db.Model("im_feedback_submissions").Safe().Ctx(ctx).
|
||||
Fields(
|
||||
"'feedback' type",
|
||||
"id",
|
||||
"feedback_id order_id",
|
||||
"user_id",
|
||||
"'' target_type",
|
||||
"'' target_summary",
|
||||
"category",
|
||||
"content",
|
||||
"contact",
|
||||
"attachment_file_ids evidence_json",
|
||||
"client_context client_context_json",
|
||||
"include_diagnostics",
|
||||
"idempotency_key",
|
||||
"status",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
).
|
||||
Where("id", id).
|
||||
Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取IM反馈工单信息失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Status 更新IM举报反馈工单状态
|
||||
func (s *sSysImWorkOrder) Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
db, _, err := imtenant.DB(ctx, in.TenantId)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
table := "im_abuse_reports"
|
||||
if in.Type == sysin.ImWorkOrderTypeFeedback {
|
||||
table = "im_feedback_submissions"
|
||||
}
|
||||
if _, err = db.Model(table).Safe().Ctx(ctx).
|
||||
Where("id", in.Id).
|
||||
Data(g.Map{
|
||||
"status": in.Status,
|
||||
"updated_at": gtime.Now(),
|
||||
}).
|
||||
Update(); err != nil {
|
||||
return gerror.Wrap(err, "更新IM举报反馈工单状态失败,请稍后重试!")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
|
||||
module = mctx.Module
|
||||
clientIp = location.GetClientIp(request)
|
||||
postData = gjson.New(consts.NilJsonToString)
|
||||
getData = gjson.New(request.URL.Query())
|
||||
getData = s.MaskSensitiveJson(gjson.New(request.URL.Query()))
|
||||
headerData = gjson.New(consts.NilJsonToString)
|
||||
errorData = gjson.New(consts.NilJsonToString)
|
||||
data entity.SysLog
|
||||
@@ -194,7 +194,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
|
||||
|
||||
// post参数
|
||||
if body, ok := mctx.Data["request.body"].(*gjson.Json); ok {
|
||||
postData = body
|
||||
postData = s.MaskSensitiveJson(body)
|
||||
}
|
||||
|
||||
// post表单
|
||||
@@ -208,12 +208,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
|
||||
if postData.IsNil() || len(postData.Map()) == 0 {
|
||||
postData = gjson.New(consts.NilJsonToString)
|
||||
} else {
|
||||
// 隐藏明文显示的密码
|
||||
for k := range postData.Map() {
|
||||
if gstr.ContainsI(k, "password") {
|
||||
postData.MustSet(k, "******")
|
||||
}
|
||||
}
|
||||
postData = s.MaskSensitiveJson(postData)
|
||||
}
|
||||
|
||||
// 当前登录用户
|
||||
@@ -247,7 +242,7 @@ func (s *sSysLog) AnalysisLog(ctx context.Context) entity.SysLog {
|
||||
Url: request.URL.Path,
|
||||
GetData: getData,
|
||||
PostData: postData,
|
||||
HeaderData: s.SimplifyHeaderParams(headerData),
|
||||
HeaderData: s.SimplifyHeaderParams(s.MaskSensitiveJson(headerData)),
|
||||
Ip: clientIp,
|
||||
ProvinceId: ipData.ProvinceCode,
|
||||
CityId: ipData.CityCode,
|
||||
@@ -273,6 +268,10 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json {
|
||||
var filters = []string{"Accept", "Authorization", "Cookie"}
|
||||
for _, filter := range filters {
|
||||
v := data.Get(filter)
|
||||
if isSensitiveLogField(filter) {
|
||||
data.MustSet(filter, "******")
|
||||
continue
|
||||
}
|
||||
if len(v.String()) > 128 {
|
||||
data.MustRemove(filter)
|
||||
}
|
||||
@@ -280,6 +279,58 @@ func (s *sSysLog) SimplifyHeaderParams(data *gjson.Json) *gjson.Json {
|
||||
return data
|
||||
}
|
||||
|
||||
// MaskSensitiveJson masks sensitive request fields before access logs are stored.
|
||||
func (s *sSysLog) MaskSensitiveJson(data *gjson.Json) *gjson.Json {
|
||||
if data == nil || data.IsNil() {
|
||||
return data
|
||||
}
|
||||
return gjson.New(maskSensitiveValue(data.Map()))
|
||||
}
|
||||
|
||||
func maskSensitiveValue(v interface{}) interface{} {
|
||||
switch value := v.(type) {
|
||||
case map[string]interface{}:
|
||||
res := make(map[string]interface{}, len(value))
|
||||
for k, item := range value {
|
||||
if isSensitiveLogField(k) {
|
||||
res[k] = "******"
|
||||
continue
|
||||
}
|
||||
res[k] = maskSensitiveValue(item)
|
||||
}
|
||||
return res
|
||||
case map[string][]string:
|
||||
res := make(map[string]interface{}, len(value))
|
||||
for k, item := range value {
|
||||
if isSensitiveLogField(k) {
|
||||
res[k] = "******"
|
||||
continue
|
||||
}
|
||||
res[k] = item
|
||||
}
|
||||
return res
|
||||
case []interface{}:
|
||||
for i, item := range value {
|
||||
value[i] = maskSensitiveValue(item)
|
||||
}
|
||||
return value
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func isSensitiveLogField(field string) bool {
|
||||
sensitive := []string{"password", "invitecode", "invite_code", "servicesecret", "service_secret", "authorization", "x-service-secret"}
|
||||
normalized := gstr.Replace(gstr.Replace(gstr.ToLower(field), "-", ""), "_", "")
|
||||
for _, item := range sensitive {
|
||||
item = gstr.Replace(gstr.Replace(item, "-", ""), "_", "")
|
||||
if normalized == item || gstr.Contains(normalized, item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// View 获取指定请求日志信息
|
||||
func (s *sSysLog) View(ctx context.Context, in *sysin.LogViewInp) (res *sysin.LogViewModel, err error) {
|
||||
mod := s.Model(ctx)
|
||||
|
||||
@@ -22,6 +22,7 @@ type Context struct {
|
||||
// Identity 通用身份模型
|
||||
type Identity struct {
|
||||
Id int64 `json:"id" description:"用户ID"`
|
||||
TenantId int64 `json:"tenantId" description:"IM租户ID"`
|
||||
Pid int64 `json:"pid" description:"上级ID"`
|
||||
DeptId int64 `json:"deptId" description:"部门ID"`
|
||||
DeptType string `json:"deptType" description:"部门类型"`
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type AdminMember struct {
|
||||
g.Meta `orm:"table:hg_admin_member, do:true"`
|
||||
Id any // 管理员ID
|
||||
TenantId any // IM租户ID
|
||||
DeptId any // 部门ID
|
||||
RoleId any // 角色ID
|
||||
RealName any // 真实姓名
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package do
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImTenant is the golang structure of table im_tenant for DAO operations like Where/Data.
|
||||
type ImTenant struct {
|
||||
g.Meta `orm:"table:im_tenant, do:true"`
|
||||
Id any //
|
||||
Code any //
|
||||
Name any //
|
||||
TenantDesc any //
|
||||
DbName any //
|
||||
DbDsn any //
|
||||
State any //
|
||||
CreatedAt *gtime.Time //
|
||||
CreatedBy any //
|
||||
UpdatedAt *gtime.Time //
|
||||
UpdatedBy any //
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package do
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImTenantConfig is the golang structure of table im_tenant_config for DAO operations like Where/Data.
|
||||
type ImTenantConfig struct {
|
||||
g.Meta `orm:"table:im_tenant_config, do:true"`
|
||||
Id any //
|
||||
TenantId any //
|
||||
MaxUsers any // 0 means unlimited
|
||||
MaxGroups any // 0 means unlimited
|
||||
MaxGroupMembers any // 0 means unlimited
|
||||
CreatedAt *gtime.Time //
|
||||
CreatedBy any //
|
||||
UpdatedAt *gtime.Time //
|
||||
UpdatedBy any //
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
// AdminMember is the golang structure for table admin_member.
|
||||
type AdminMember struct {
|
||||
Id int64 `json:"id" orm:"id" description:"管理员ID"`
|
||||
TenantId int64 `json:"tenantId" orm:"tenant_id" description:"IM租户ID"`
|
||||
DeptId int64 `json:"deptId" orm:"dept_id" description:"部门ID"`
|
||||
RoleId int64 `json:"roleId" orm:"role_id" description:"角色ID"`
|
||||
RealName string `json:"realName" orm:"real_name" description:"真实姓名"`
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImTenant is the golang structure for table im_tenant.
|
||||
type ImTenant struct {
|
||||
Id int64 `json:"id" orm:"id" description:""`
|
||||
Code string `json:"code" orm:"code" description:""`
|
||||
Name string `json:"name" orm:"name" description:""`
|
||||
TenantDesc string `json:"tenantDesc" orm:"tenant_desc" description:""`
|
||||
DbName string `json:"dbName" orm:"db_name" description:""`
|
||||
DbDsn string `json:"dbDsn" orm:"db_dsn" description:""`
|
||||
State int `json:"state" orm:"state" description:""`
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
|
||||
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""`
|
||||
UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package entity
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImTenantConfig is the golang structure for table im_tenant_config.
|
||||
type ImTenantConfig struct {
|
||||
Id int64 `json:"id" orm:"id" description:""`
|
||||
TenantId int64 `json:"tenantId" orm:"tenant_id" description:""`
|
||||
MaxUsers uint64 `json:"maxUsers" orm:"max_users" description:"0 means unlimited"`
|
||||
MaxGroups uint64 `json:"maxGroups" orm:"max_groups" description:"0 means unlimited"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" orm:"max_group_members" description:"0 means unlimited"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""`
|
||||
CreatedBy int64 `json:"createdBy" orm:"created_by" description:""`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""`
|
||||
UpdatedBy int64 `json:"updatedBy" orm:"updated_by" description:""`
|
||||
}
|
||||
@@ -115,6 +115,7 @@ type LoginMemberInfoModel struct {
|
||||
// MemberEditInp 修改用户
|
||||
type MemberEditInp struct {
|
||||
Id int64 `json:"id" dc:"管理员ID"`
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
RoleId int64 `json:"roleId" v:"required#角色不能为空" dc:"角色ID"`
|
||||
PostIds []int64 `json:"postIds" dc:"岗位ID"`
|
||||
DeptId int64 `json:"deptId" v:"required#部门不能为空" dc:"部门ID"`
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImAuditLogListInp 获取IM审计日志列表
|
||||
type ImAuditLogListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
ActorUid int64 `json:"actorUid" dc:"操作人UID"`
|
||||
ActorRole string `json:"actorRole" dc:"操作角色"`
|
||||
Action string `json:"action" dc:"操作类型"`
|
||||
TargetType string `json:"targetType" dc:"目标类型"`
|
||||
TargetId string `json:"targetId" dc:"目标ID"`
|
||||
RequestId string `json:"requestId" dc:"请求ID"`
|
||||
Ip string `json:"ip" dc:"IP"`
|
||||
Keyword string `json:"keyword" dc:"关键词"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
func (in *ImAuditLogListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImAuditLogListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
ActorUid int64 `json:"actorUid" dc:"操作人UID"`
|
||||
ActorRole string `json:"actorRole" dc:"操作角色"`
|
||||
Action string `json:"action" dc:"操作类型"`
|
||||
TargetType string `json:"targetType" dc:"目标类型"`
|
||||
TargetId string `json:"targetId" dc:"目标ID"`
|
||||
Reason string `json:"reason" dc:"操作原因"`
|
||||
RequestId string `json:"requestId" dc:"请求ID"`
|
||||
Ip string `json:"ip" dc:"IP"`
|
||||
UserAgent string `json:"userAgent" dc:"客户端"`
|
||||
BeforeSnapshot string `json:"beforeSnapshot" dc:"变更前快照"`
|
||||
AfterSnapshot string `json:"afterSnapshot" dc:"变更后快照"`
|
||||
Metadata string `json:"metadata" dc:"元数据"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
// ImAuditLogViewInp 获取指定IM审计日志信息
|
||||
type ImAuditLogViewInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
}
|
||||
|
||||
func (in *ImAuditLogViewInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImAuditLogViewModel struct {
|
||||
ImAuditLogListModel
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImGroupListInp 获取IM群组列表
|
||||
type ImGroupListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
OwnerId int64 `json:"ownerId" dc:"群主ID"`
|
||||
State *int `json:"state" dc:"通信状态"`
|
||||
GroupStatus string `json:"groupStatus" dc:"业务状态"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
TouchedAt []*gtime.Time `json:"touchedAt" dc:"活跃时间"`
|
||||
}
|
||||
|
||||
func (in *ImGroupListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
OwnerId int64 `json:"ownerId" dc:"群主ID"`
|
||||
SeqId int `json:"seqId" dc:"最新消息序号"`
|
||||
DelId int `json:"delId" dc:"删除序号"`
|
||||
SubCnt int `json:"subCnt" dc:"成员数"`
|
||||
TouchedAt *gtime.Time `json:"touchedAt" dc:"活跃时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
PublicJson string `json:"publicJson" dc:"公开资料"`
|
||||
TrustedJson string `json:"trustedJson" dc:"可信资料"`
|
||||
TagsJson string `json:"tagsJson" dc:"标签"`
|
||||
AuxJson string `json:"auxJson" dc:"辅助资料"`
|
||||
|
||||
GroupName string `json:"groupName" dc:"群名称"`
|
||||
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
|
||||
Description string `json:"description" dc:"简介"`
|
||||
GroupStatus string `json:"groupStatus" dc:"业务状态"`
|
||||
JoinApproval int `json:"joinApproval" dc:"入群审批"`
|
||||
MuteAll int `json:"muteAll" dc:"全员禁言"`
|
||||
MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"`
|
||||
MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"`
|
||||
MemberInvite int `json:"memberInvite" dc:"成员邀请"`
|
||||
MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"`
|
||||
MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"`
|
||||
BusinessMemberCount int `json:"businessMemberCount" dc:"业务成员数"`
|
||||
SettingsVersion int `json:"settingsVersion" dc:"治理配置版本"`
|
||||
SettingsUpdatedAt *gtime.Time `json:"settingsUpdatedAt" dc:"治理配置更新时间"`
|
||||
}
|
||||
|
||||
// ImGroupViewInp 获取指定IM群组信息
|
||||
type ImGroupViewInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
|
||||
}
|
||||
|
||||
func (in *ImGroupViewInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Topic == "" {
|
||||
return gerror.New("Topic不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupViewModel struct {
|
||||
ImGroupListModel
|
||||
AccessJson string `json:"accessJson" dc:"访问权限"`
|
||||
UseBt int `json:"useBt" dc:"是否开启BT"`
|
||||
}
|
||||
|
||||
// ImGroupMemberListInp 获取IM群成员列表
|
||||
type ImGroupMemberListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
Role string `json:"role" dc:"角色"`
|
||||
MemberStatus string `json:"memberStatus" dc:"成员状态"`
|
||||
}
|
||||
|
||||
func (in *ImGroupMemberListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Topic == "" {
|
||||
return gerror.New("Topic不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupMemberListModel struct {
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
State int `json:"state" dc:"用户状态"`
|
||||
ModeWant string `json:"modeWant" dc:"期望权限"`
|
||||
ModeGiven string `json:"modeGiven" dc:"授予权限"`
|
||||
RecvSeqId int `json:"recvSeqId" dc:"接收序号"`
|
||||
ReadSeqId int `json:"readSeqId" dc:"已读序号"`
|
||||
DelId int `json:"delId" dc:"删除序号"`
|
||||
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"加入时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
PrivateJson string `json:"privateJson" dc:"私有资料"`
|
||||
Role string `json:"role" dc:"业务角色"`
|
||||
MemberStatus string `json:"memberStatus" dc:"业务状态"`
|
||||
GroupNickname string `json:"groupNickname" dc:"群昵称"`
|
||||
Pinned int `json:"pinned" dc:"置顶"`
|
||||
MuteNotifications int `json:"muteNotifications" dc:"免打扰"`
|
||||
JoinSource string `json:"joinSource" dc:"加入来源"`
|
||||
JoinedBy int64 `json:"joinedBy" dc:"邀请/处理人"`
|
||||
JoinedAt *gtime.Time `json:"joinedAt" dc:"业务加入时间"`
|
||||
LeftAt *gtime.Time `json:"leftAt" dc:"离开时间"`
|
||||
RemovedBy int64 `json:"removedBy" dc:"移除人"`
|
||||
Version int `json:"version" dc:"版本"`
|
||||
}
|
||||
|
||||
// ImGroupTopicInp 群Topic通用入参
|
||||
type ImGroupTopicInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Topic string `json:"topic" v:"required#Topic不能为空" dc:"Topic"`
|
||||
}
|
||||
|
||||
func (in *ImGroupTopicInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Topic == "" {
|
||||
return gerror.New("Topic不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImGroupGovernanceModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
OwnerUserId int64 `json:"ownerUserId" dc:"群主ID"`
|
||||
Name string `json:"name" dc:"群名称"`
|
||||
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
|
||||
Description string `json:"description" dc:"简介"`
|
||||
GroupStatus string `json:"groupStatus" dc:"业务状态"`
|
||||
JoinApproval int `json:"joinApproval" dc:"入群审批"`
|
||||
MuteAll int `json:"muteAll" dc:"全员禁言"`
|
||||
MemberUploadFiles int `json:"memberUploadFiles" dc:"成员上传文件"`
|
||||
MemberPrivateChat int `json:"memberPrivateChat" dc:"成员私聊"`
|
||||
MemberInvite int `json:"memberInvite" dc:"成员邀请"`
|
||||
MaxMemberCount int `json:"maxMemberCount" dc:"最大成员数"`
|
||||
MaxAdminCount int `json:"maxAdminCount" dc:"最大管理员数"`
|
||||
MemberCount int `json:"memberCount" dc:"成员数"`
|
||||
Version int `json:"version" dc:"版本"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建人"`
|
||||
DissolvedBy int64 `json:"dissolvedBy" dc:"解散人"`
|
||||
DissolvedAt *gtime.Time `json:"dissolvedAt" dc:"解散时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
type ImGroupJoinRequestListInp struct {
|
||||
form.PageReq
|
||||
ImGroupTopicInp
|
||||
Status string `json:"status" dc:"状态"`
|
||||
ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"`
|
||||
}
|
||||
|
||||
type ImGroupJoinRequestListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
RequestId string `json:"requestId" dc:"申请ID"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
ApplicantUserId int64 `json:"applicantUserId" dc:"申请人ID"`
|
||||
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
|
||||
Source string `json:"source" dc:"来源"`
|
||||
Message string `json:"message" dc:"申请消息"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
RejectReason string `json:"rejectReason" dc:"拒绝原因"`
|
||||
HandledBy int64 `json:"handledBy" dc:"处理人"`
|
||||
HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
|
||||
Version int `json:"version" dc:"版本"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
type ImGroupInvitationListInp struct {
|
||||
form.PageReq
|
||||
ImGroupTopicInp
|
||||
Status string `json:"status" dc:"状态"`
|
||||
TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"`
|
||||
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
|
||||
}
|
||||
|
||||
type ImGroupInvitationListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
InvitationId string `json:"invitationId" dc:"邀请ID"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
InviterUserId int64 `json:"inviterUserId" dc:"邀请人ID"`
|
||||
TargetUserId int64 `json:"targetUserId" dc:"目标用户ID"`
|
||||
Message string `json:"message" dc:"邀请消息"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
Result string `json:"result" dc:"结果"`
|
||||
JoinRequestId string `json:"joinRequestId" dc:"入群申请ID"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
|
||||
HandledAt *gtime.Time `json:"handledAt" dc:"处理时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
type ImGroupAnnouncementListInp struct {
|
||||
form.PageReq
|
||||
ImGroupTopicInp
|
||||
Status string `json:"status" dc:"状态"`
|
||||
}
|
||||
|
||||
type ImGroupAnnouncementListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
AnnouncementId string `json:"announcementId" dc:"公告ID"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
AuthorUserId int64 `json:"authorUserId" dc:"发布人ID"`
|
||||
Content string `json:"content" dc:"内容"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
Version int `json:"version" dc:"版本"`
|
||||
PublishedAt *gtime.Time `json:"publishedAt" dc:"发布时间"`
|
||||
DeletedBy int64 `json:"deletedBy" dc:"删除人"`
|
||||
DeletedAt *gtime.Time `json:"deletedAt" dc:"删除时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
type ImGroupMuteListInp struct {
|
||||
form.PageReq
|
||||
ImGroupTopicInp
|
||||
Status string `json:"status" dc:"状态"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
}
|
||||
|
||||
type ImGroupMuteListModel struct {
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
MutedBy int64 `json:"mutedBy" dc:"禁言人"`
|
||||
MutedAt *gtime.Time `json:"mutedAt" dc:"禁言时间"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" dc:"过期时间"`
|
||||
Reason string `json:"reason" dc:"原因"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
RevokedBy int64 `json:"revokedBy" dc:"解除人"`
|
||||
RevokedAt *gtime.Time `json:"revokedAt" dc:"解除时间"`
|
||||
Version int `json:"version" dc:"版本"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
type ImGroupAdminLogListInp struct {
|
||||
form.PageReq
|
||||
ImGroupTopicInp
|
||||
Action string `json:"action" dc:"动作"`
|
||||
OperatorId int64 `json:"operatorId" dc:"操作人ID"`
|
||||
}
|
||||
|
||||
type ImGroupAdminLogListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
LogId string `json:"logId" dc:"日志ID"`
|
||||
Topic string `json:"topic" dc:"Topic"`
|
||||
OperatorId int64 `json:"operatorId" dc:"操作人"`
|
||||
TargetUserId int64 `json:"targetUserId" dc:"目标用户"`
|
||||
Action string `json:"action" dc:"动作"`
|
||||
ObjectType string `json:"objectType" dc:"对象类型"`
|
||||
ObjectId string `json:"objectId" dc:"对象ID"`
|
||||
BeforeData string `json:"beforeData" dc:"变更前"`
|
||||
AfterData string `json:"afterData" dc:"变更后"`
|
||||
RequestId string `json:"requestId" dc:"请求ID"`
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type ImInviteListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
InviteCode string `json:"inviteCode" dc:"完整邀请码"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
BatchId string `json:"batchId" dc:"批次ID"`
|
||||
CreatedBy string `json:"createdBy" dc:"创建人"`
|
||||
CreatedAtStart *gtime.Time `json:"createdAtStart" dc:"创建开始时间"`
|
||||
CreatedAtEnd *gtime.Time `json:"createdAtEnd" dc:"创建结束时间"`
|
||||
ExpiresAtStart *gtime.Time `json:"expiresAtStart" dc:"过期开始时间"`
|
||||
ExpiresAtEnd *gtime.Time `json:"expiresAtEnd" dc:"过期结束时间"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
ExpiresAt []*gtime.Time `json:"expiresAt" dc:"到期时间"`
|
||||
}
|
||||
|
||||
func (in *ImInviteListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.Page <= 0 {
|
||||
in.Page = 1
|
||||
}
|
||||
if in.PerPage <= 0 {
|
||||
in.PerPage = 20
|
||||
}
|
||||
if in.PerPage > 100 {
|
||||
return gerror.New("每页数量最大值不能大于100")
|
||||
}
|
||||
if in.InviteCode != "" && len(in.InviteCode) != 5 {
|
||||
return gerror.New("邀请码必须为完整5位")
|
||||
}
|
||||
if len(in.CreatedAt) == 2 {
|
||||
in.CreatedAtStart = in.CreatedAt[0]
|
||||
in.CreatedAtEnd = in.CreatedAt[1]
|
||||
}
|
||||
if len(in.ExpiresAt) == 2 {
|
||||
in.ExpiresAtStart = in.ExpiresAt[0]
|
||||
in.ExpiresAtEnd = in.ExpiresAt[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImInviteCreateInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
||||
Remark string `json:"remark" dc:"备注"`
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
}
|
||||
|
||||
func (in *ImInviteCreateInp) Filter(ctx context.Context) (err error) {
|
||||
in.IdempotencyKey = strings.TrimSpace(in.IdempotencyKey)
|
||||
if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) {
|
||||
return gerror.New("到期时间必须晚于当前时间")
|
||||
}
|
||||
if len(in.Remark) > 255 {
|
||||
return gerror.New("备注不能超过255个字符")
|
||||
}
|
||||
if len(in.IdempotencyKey) > 128 {
|
||||
return gerror.New("幂等键不能超过128个字符")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImInviteBatchCreateInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
Quantity int `json:"quantity" dc:"数量"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" v:"required#到期时间不能为空" dc:"到期时间"`
|
||||
Remark string `json:"remark" dc:"备注"`
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
}
|
||||
|
||||
func (in *ImInviteBatchCreateInp) Filter(ctx context.Context) (err error) {
|
||||
in.IdempotencyKey = strings.TrimSpace(in.IdempotencyKey)
|
||||
if in.Quantity < 1 || in.Quantity > 1000 {
|
||||
return gerror.New("批量生成数量必须在1到1000之间")
|
||||
}
|
||||
if in.ExpiresAt == nil || in.ExpiresAt.Before(gtime.Now()) {
|
||||
return gerror.New("到期时间必须晚于当前时间")
|
||||
}
|
||||
if len(in.Remark) > 255 {
|
||||
return gerror.New("备注不能超过255个字符")
|
||||
}
|
||||
if len(in.IdempotencyKey) == 0 || len(in.IdempotencyKey) > 128 {
|
||||
return gerror.New("批量生成必须提供不超过128个字符的幂等键")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImInviteBatchViewInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"`
|
||||
}
|
||||
|
||||
type ImInviteRevokeInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
Id string `json:"id" v:"required#邀请码ID不能为空" dc:"邀请码ID"`
|
||||
}
|
||||
|
||||
type ImInviteBatchRevokeInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"IM租户ID"`
|
||||
BatchId string `json:"batchId" v:"required#批次ID不能为空" dc:"批次ID"`
|
||||
}
|
||||
|
||||
type ImInviteCodeModel struct {
|
||||
Id string `json:"id" dc:"ID"`
|
||||
TenantId string `json:"tenantId" dc:"IM租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"IM租户名称"`
|
||||
BatchId string `json:"batchId" dc:"批次ID"`
|
||||
Code string `json:"code" dc:"邀请码"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
|
||||
UsageCount int `json:"usageCount" dc:"累计使用次数"`
|
||||
LastUsedAt *gtime.Time `json:"lastUsedAt" dc:"最后使用时间"`
|
||||
CreatedBy string `json:"createdBy" dc:"创建人"`
|
||||
Remark string `json:"remark" dc:"备注"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
type ImInviteBatchModel struct {
|
||||
BatchId string `json:"batchId" dc:"批次ID"`
|
||||
TenantId string `json:"tenantId" dc:"IM租户ID"`
|
||||
Quantity int `json:"quantity" dc:"数量"`
|
||||
ActiveCount int `json:"activeCount" dc:"有效数量"`
|
||||
UsageCount int `json:"usageCount" dc:"累计使用次数"`
|
||||
RevokedCount int `json:"revokedCount" dc:"已撤销数量"`
|
||||
ExpiredCount int `json:"expiredCount" dc:"已过期数量"`
|
||||
ExpiresAt *gtime.Time `json:"expiresAt" dc:"到期时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
type ImInviteBatchDetailModel struct {
|
||||
ImInviteBatchModel
|
||||
List []*ImInviteCodeModel `json:"list" dc:"批次邀请码"`
|
||||
}
|
||||
|
||||
func ParseOptionalInt64(value string) (int64, error) {
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, gerror.New("ID格式不正确")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImRealnameListInp 获取IM实名审核列表
|
||||
type ImRealnameListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
SubmissionId string `json:"submissionId" dc:"提交ID"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
func (in *ImRealnameListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImRealnameListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
UserId int64 `json:"userId" dc:"用户ID"`
|
||||
SubmissionId string `json:"submissionId" dc:"提交ID"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
MaskedName string `json:"maskedName" dc:"脱敏姓名"`
|
||||
MaskedIdNumber string `json:"maskedIdNumber" dc:"脱敏证件号"`
|
||||
IdType string `json:"idType" dc:"证件类型"`
|
||||
ReviewReason string `json:"reviewReason" dc:"审核原因"`
|
||||
VerifiedAt *gtime.Time `json:"verifiedAt" dc:"通过时间"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
// ImRealnameViewInp 获取指定IM实名审核信息
|
||||
type ImRealnameViewInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
}
|
||||
|
||||
func (in *ImRealnameViewInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImRealnameViewModel struct {
|
||||
ImRealnameListModel
|
||||
IdNumberHash string `json:"idNumberHash" dc:"证件号哈希"`
|
||||
}
|
||||
|
||||
// ImRealnameReviewInp 审核IM实名
|
||||
type ImRealnameReviewInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
Status string `json:"status" dc:"审核状态"`
|
||||
ReviewReason string `json:"reviewReason" dc:"审核原因"`
|
||||
}
|
||||
|
||||
func (in *ImRealnameReviewInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
if !dict.HasOptionKey(consts.ImRealnameStatusOptions, in.Status) {
|
||||
return gerror.New("审核状态不正确")
|
||||
}
|
||||
if in.Status == consts.ImRealnameStatusRejected && in.ReviewReason == "" {
|
||||
return gerror.New("拒绝原因不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImRealnameReviewModel struct{}
|
||||
@@ -0,0 +1,12 @@
|
||||
package sysin
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
type ImRegistrationConfigModel struct {
|
||||
RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"`
|
||||
}
|
||||
|
||||
type ImRegistrationConfigUpdateInp struct {
|
||||
RegisterSmsEnabled bool `json:"registerSmsEnabled" dc:"注册短信是否真实发送"`
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/library/hgorm/hook"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImTenantUpdateFields 修改IM租户字段过滤
|
||||
type ImTenantUpdateFields struct {
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
DbDsn string `json:"dbDsn" dc:"独立连接串"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
}
|
||||
|
||||
// ImTenantInsertFields 新增IM租户字段过滤
|
||||
type ImTenantInsertFields struct {
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
DbDsn string `json:"dbDsn" dc:"独立连接串"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
}
|
||||
|
||||
// ImTenantConfigUpdateFields 修改IM租户配置字段过滤
|
||||
type ImTenantConfigUpdateFields struct {
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
}
|
||||
|
||||
// ImTenantConfigInsertFields 新增IM租户配置字段过滤
|
||||
type ImTenantConfigInsertFields struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
}
|
||||
|
||||
// ImTenantEditInp 修改/新增IM租户
|
||||
type ImTenantEditInp struct {
|
||||
entity.ImTenant
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
}
|
||||
|
||||
func (in *ImTenantEditInp) Filter(ctx context.Context) (err error) {
|
||||
if e := g.Validator().Rules("required|length:2,64").Data(in.Code).Messages("租户编码不能为空|租户编码长度应为2到64个字符").Run(ctx); e != nil {
|
||||
return e.Current()
|
||||
}
|
||||
if e := g.Validator().Rules("required|max-length:128").Data(in.Name).Messages("租户名称不能为空|租户名称不能超过128个字符").Run(ctx); e != nil {
|
||||
return e.Current()
|
||||
}
|
||||
if in.DbName == "" && in.DbDsn == "" {
|
||||
return gerror.New("业务库名和独立连接串至少填写一个")
|
||||
}
|
||||
if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) {
|
||||
return gerror.New("租户状态不正确")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImTenantEditModel struct{}
|
||||
|
||||
// ImTenantViewInp 获取指定IM租户信息
|
||||
type ImTenantViewInp struct {
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
}
|
||||
|
||||
func (in *ImTenantViewInp) Filter(ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type ImTenantViewModel struct {
|
||||
entity.ImTenant
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
TenantDbReachable bool `json:"tenantDbReachable" dc:"业务库是否可达"`
|
||||
TenantDbCheckError string `json:"tenantDbCheckError" dc:"业务库检查错误"`
|
||||
}
|
||||
|
||||
// ImTenantListInp 获取IM租户列表
|
||||
type ImTenantListInp struct {
|
||||
form.PageReq
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
State *int `json:"state" dc:"状态"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
func (in *ImTenantListInp) Filter(ctx context.Context) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type ImTenantListModel struct {
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
Code string `json:"code" dc:"租户编码"`
|
||||
Name string `json:"name" dc:"租户名称"`
|
||||
TenantDesc string `json:"tenantDesc" dc:"租户描述"`
|
||||
DbName string `json:"dbName" dc:"业务库名"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
MaxUsers uint64 `json:"maxUsers" dc:"最大用户数"`
|
||||
MaxGroups uint64 `json:"maxGroups" dc:"最大群组数"`
|
||||
MaxGroupMembers uint `json:"maxGroupMembers" dc:"最大群成员数"`
|
||||
DbDsnConfigured bool `json:"dbDsnConfigured" dc:"是否配置独立连接串"`
|
||||
CreatedBy int64 `json:"createdBy" dc:"创建者"`
|
||||
CreatedBySumma *hook.MemberSumma `json:"createdBySumma" dc:"创建者摘要信息"`
|
||||
UpdatedBy int64 `json:"updatedBy" dc:"更新者"`
|
||||
UpdatedBySumma *hook.MemberSumma `json:"updatedBySumma" dc:"更新者摘要信息"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
// ImTenantStatusInp 更新IM租户状态
|
||||
type ImTenantStatusInp struct {
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
State int `json:"state" dc:"状态"`
|
||||
}
|
||||
|
||||
func (in *ImTenantStatusInp) Filter(ctx context.Context) (err error) {
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
if !dict.HasOptionKey(consts.ImTenantStateOptions, in.State) {
|
||||
return gerror.New("租户状态不正确")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImTenantStatusModel struct{}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
// ImUserListInp 获取IM用户列表
|
||||
type ImUserListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId string `json:"userId" dc:"用户ID"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
AuthName string `json:"authName" dc:"登录标识"`
|
||||
State *int `json:"state" dc:"账号状态"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
LastSeen []*gtime.Time `json:"lastSeen" dc:"最后在线时间"`
|
||||
}
|
||||
|
||||
func (in *ImUserListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.UserId != "" {
|
||||
if _, err = ParseOptionalInt64(in.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImUserListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
UserId string `json:"userId" dc:"用户ID"`
|
||||
Username string `json:"username" dc:"登录账号"`
|
||||
Phone string `json:"phone" dc:"手机号"`
|
||||
Nickname string `json:"nickname" dc:"昵称"`
|
||||
PublicAccount string `json:"publicAccount" dc:"公开账号"`
|
||||
Gender string `json:"gender" dc:"性别"`
|
||||
Region string `json:"region" dc:"地区"`
|
||||
State int `json:"state" dc:"账号状态"`
|
||||
AuthVersion int `json:"authVersion" dc:"认证版本"`
|
||||
PasswordUpdatedAt *gtime.Time `json:"passwordUpdatedAt" dc:"密码更新时间"`
|
||||
AuthNames string `json:"authNames" dc:"登录标识"`
|
||||
Credentials string `json:"credentials" dc:"凭证"`
|
||||
LastSeen *gtime.Time `json:"lastSeen" dc:"最后在线时间"`
|
||||
UserAgent string `json:"userAgent" dc:"客户端"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
// ImUserViewInp 获取指定IM用户信息
|
||||
type ImUserViewInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
|
||||
}
|
||||
|
||||
func (in *ImUserViewInp) Filter(ctx context.Context) (err error) {
|
||||
if _, err = ParseOptionalInt64(in.UserId); err != nil {
|
||||
return gerror.New("用户ID不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImUserPasswordResetInp struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
UserId string `json:"userId" v:"required#用户ID不能为空" dc:"用户ID"`
|
||||
NewPassword string `json:"newPassword" v:"required#新密码不能为空" dc:"新密码"`
|
||||
}
|
||||
|
||||
func (in *ImUserPasswordResetInp) Filter(ctx context.Context) error {
|
||||
if _, err := ParseOptionalInt64(in.UserId); err != nil {
|
||||
return err
|
||||
}
|
||||
if len([]rune(in.NewPassword)) < 3 {
|
||||
return gerror.New("新密码长度不能少于3位")
|
||||
}
|
||||
if len([]byte(in.NewPassword)) > 72 {
|
||||
return gerror.New("新密码长度不能超过72字节")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImUserViewModel struct {
|
||||
ImUserListModel
|
||||
Signature string `json:"signature" dc:"个性签名"`
|
||||
AvatarFileId string `json:"avatarFileId" dc:"头像文件ID"`
|
||||
BirthDate string `json:"birthDate" dc:"出生日期"`
|
||||
ProfileJson string `json:"profileJson" dc:"Tinode Public资料"`
|
||||
TrustedJson string `json:"trustedJson" dc:"Tinode Trusted资料"`
|
||||
AccessJson string `json:"accessJson" dc:"访问权限"`
|
||||
ProfileVersion int `json:"profileVersion" dc:"资料版本"`
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Package sysin
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package sysin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"hotgo/internal/consts"
|
||||
"hotgo/internal/library/dict"
|
||||
"hotgo/internal/model/input/form"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
const (
|
||||
ImWorkOrderTypeAbuse = "abuse"
|
||||
ImWorkOrderTypeFeedback = "feedback"
|
||||
)
|
||||
|
||||
// ImWorkOrderListInp 获取IM举报反馈工单列表
|
||||
type ImWorkOrderListInp struct {
|
||||
form.PageReq
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
Type string `json:"type" dc:"工单类型"`
|
||||
UserId int64 `json:"userId" dc:"提交人ID"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
Keyword string `json:"keyword" dc:"关键词"`
|
||||
CreatedAt []*gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
}
|
||||
|
||||
func (in *ImWorkOrderListInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Type == "" {
|
||||
return gerror.New("请选择工单类型")
|
||||
}
|
||||
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
|
||||
return gerror.New("工单类型不正确")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImWorkOrderListModel struct {
|
||||
TenantId int64 `json:"tenantId" dc:"租户ID"`
|
||||
TenantName string `json:"tenantName" dc:"租户名称"`
|
||||
Type string `json:"type" dc:"工单类型"`
|
||||
Id int64 `json:"id" dc:"ID"`
|
||||
OrderId string `json:"orderId" dc:"工单ID"`
|
||||
UserId int64 `json:"userId" dc:"提交人ID"`
|
||||
TargetType string `json:"targetType" dc:"目标类型"`
|
||||
TargetSummary string `json:"targetSummary" dc:"目标摘要"`
|
||||
Category string `json:"category" dc:"分类"`
|
||||
Content string `json:"content" dc:"内容"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"`
|
||||
UpdatedAt *gtime.Time `json:"updatedAt" dc:"修改时间"`
|
||||
}
|
||||
|
||||
// ImWorkOrderViewInp 获取指定IM举报反馈工单信息
|
||||
type ImWorkOrderViewInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"`
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
}
|
||||
|
||||
func (in *ImWorkOrderViewInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
|
||||
return gerror.New("工单类型不正确")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImWorkOrderViewModel struct {
|
||||
ImWorkOrderListModel
|
||||
Description string `json:"description" dc:"描述"`
|
||||
Contact string `json:"contact" dc:"联系方式"`
|
||||
EvidenceJson string `json:"evidenceJson" dc:"证据/附件"`
|
||||
SnapshotJson string `json:"snapshotJson" dc:"快照"`
|
||||
ClientContextJson string `json:"clientContextJson" dc:"客户端上下文"`
|
||||
IncludeDiagnostics int `json:"includeDiagnostics" dc:"是否包含诊断"`
|
||||
IdempotencyKey string `json:"idempotencyKey" dc:"幂等键"`
|
||||
}
|
||||
|
||||
// ImWorkOrderStatusInp 更新IM举报反馈工单状态
|
||||
type ImWorkOrderStatusInp struct {
|
||||
TenantId int64 `json:"tenantId" v:"required#租户ID不能为空" dc:"租户ID"`
|
||||
Type string `json:"type" v:"required#工单类型不能为空" dc:"工单类型"`
|
||||
Id int64 `json:"id" v:"required#ID不能为空" dc:"ID"`
|
||||
Status string `json:"status" dc:"状态"`
|
||||
}
|
||||
|
||||
func (in *ImWorkOrderStatusInp) Filter(ctx context.Context) (err error) {
|
||||
if in.TenantId <= 0 {
|
||||
return gerror.New("请选择租户")
|
||||
}
|
||||
if in.Type != ImWorkOrderTypeAbuse && in.Type != ImWorkOrderTypeFeedback {
|
||||
return gerror.New("工单类型不正确")
|
||||
}
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("ID不能为空")
|
||||
}
|
||||
if !dict.HasOptionKey(consts.ImWorkOrderStatusOptions, in.Status) {
|
||||
return gerror.New("工单状态不正确")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ImWorkOrderStatusModel struct{}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Package genrouter
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2026 HotGo
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImAuditLog) // IM审计日志
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImGroup) // IM群组
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImInvite) // IM注册邀请码
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRealname) // IM实名审核
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImRegistration) // IM注册配置
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImTenant) // IM租户
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImUser) // IM用户
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package genrouter
|
||||
|
||||
import "hotgo/internal/controller/admin/sys"
|
||||
|
||||
func init() {
|
||||
LoginRequiredRouter = append(LoginRequiredRouter, sys.ImWorkOrder) // IM举报反馈
|
||||
}
|
||||
@@ -171,6 +171,90 @@ type (
|
||||
// Switch 更新CURD列表开关
|
||||
Switch(ctx context.Context, in *sysin.CurdDemoSwitchInp) (err error)
|
||||
}
|
||||
ISysImTenant interface {
|
||||
// Model IM租户ORM模型
|
||||
Model(ctx context.Context, option ...*handler.Option) *gdb.Model
|
||||
// List 获取IM租户列表
|
||||
List(ctx context.Context, in *sysin.ImTenantListInp) (list []*sysin.ImTenantListModel, totalCount int, err error)
|
||||
// Edit 修改/新增IM租户
|
||||
Edit(ctx context.Context, in *sysin.ImTenantEditInp) (err error)
|
||||
// View 获取IM租户指定信息
|
||||
View(ctx context.Context, in *sysin.ImTenantViewInp) (res *sysin.ImTenantViewModel, err error)
|
||||
// Status 更新IM租户状态
|
||||
Status(ctx context.Context, in *sysin.ImTenantStatusInp) (err error)
|
||||
}
|
||||
ISysImRegistration interface {
|
||||
// GetConfig 获取IM注册配置
|
||||
GetConfig(ctx context.Context) (res *sysin.ImRegistrationConfigModel, err error)
|
||||
// UpdateConfig 更新IM注册配置
|
||||
UpdateConfig(ctx context.Context, in *sysin.ImRegistrationConfigUpdateInp) (err error)
|
||||
}
|
||||
ISysImInvite interface {
|
||||
// List 查询IM注册邀请码
|
||||
List(ctx context.Context, in *sysin.ImInviteListInp) (list []*sysin.ImInviteCodeModel, totalCount int, err error)
|
||||
// Create 单个生成IM注册邀请码
|
||||
Create(ctx context.Context, in *sysin.ImInviteCreateInp) (res *sysin.ImInviteCodeModel, err error)
|
||||
// BatchCreate 批量生成IM注册邀请码
|
||||
BatchCreate(ctx context.Context, in *sysin.ImInviteBatchCreateInp) (res *sysin.ImInviteBatchModel, err error)
|
||||
// BatchView 查看批次详情
|
||||
BatchView(ctx context.Context, in *sysin.ImInviteBatchViewInp) (res *sysin.ImInviteBatchDetailModel, err error)
|
||||
// Revoke 撤销邀请码
|
||||
Revoke(ctx context.Context, in *sysin.ImInviteRevokeInp) (err error)
|
||||
// BatchRevoke 批量撤销邀请码
|
||||
BatchRevoke(ctx context.Context, in *sysin.ImInviteBatchRevokeInp) (res *sysin.ImInviteBatchModel, err error)
|
||||
// Export 导出邀请码
|
||||
Export(ctx context.Context, in *sysin.ImInviteListInp) (err error)
|
||||
}
|
||||
ISysImUser interface {
|
||||
// List 获取IM用户列表
|
||||
List(ctx context.Context, in *sysin.ImUserListInp) (list []*sysin.ImUserListModel, totalCount int, err error)
|
||||
// View 获取IM用户指定信息
|
||||
View(ctx context.Context, in *sysin.ImUserViewInp) (res *sysin.ImUserViewModel, err error)
|
||||
// ResetPassword 管理员重置IM用户密码
|
||||
ResetPassword(ctx context.Context, in *sysin.ImUserPasswordResetInp) (err error)
|
||||
}
|
||||
ISysImGroup interface {
|
||||
// List 获取IM群组列表
|
||||
List(ctx context.Context, in *sysin.ImGroupListInp) (list []*sysin.ImGroupListModel, totalCount int, err error)
|
||||
// View 获取IM群组指定信息
|
||||
View(ctx context.Context, in *sysin.ImGroupViewInp) (res *sysin.ImGroupViewModel, err error)
|
||||
// MemberList 获取IM群成员列表
|
||||
MemberList(ctx context.Context, in *sysin.ImGroupMemberListInp) (list []*sysin.ImGroupMemberListModel, totalCount int, err error)
|
||||
// Governance 获取IM群治理配置
|
||||
Governance(ctx context.Context, in *sysin.ImGroupTopicInp) (res *sysin.ImGroupGovernanceModel, err error)
|
||||
// JoinRequestList 获取IM入群申请列表
|
||||
JoinRequestList(ctx context.Context, in *sysin.ImGroupJoinRequestListInp) (list []*sysin.ImGroupJoinRequestListModel, totalCount int, err error)
|
||||
// InvitationList 获取IM群邀请列表
|
||||
InvitationList(ctx context.Context, in *sysin.ImGroupInvitationListInp) (list []*sysin.ImGroupInvitationListModel, totalCount int, err error)
|
||||
// AnnouncementList 获取IM群公告列表
|
||||
AnnouncementList(ctx context.Context, in *sysin.ImGroupAnnouncementListInp) (list []*sysin.ImGroupAnnouncementListModel, totalCount int, err error)
|
||||
// MuteList 获取IM群禁言列表
|
||||
MuteList(ctx context.Context, in *sysin.ImGroupMuteListInp) (list []*sysin.ImGroupMuteListModel, totalCount int, err error)
|
||||
// AdminLogList 获取IM群管理日志列表
|
||||
AdminLogList(ctx context.Context, in *sysin.ImGroupAdminLogListInp) (list []*sysin.ImGroupAdminLogListModel, totalCount int, err error)
|
||||
}
|
||||
ISysImRealname interface {
|
||||
// List 获取IM实名审核列表
|
||||
List(ctx context.Context, in *sysin.ImRealnameListInp) (list []*sysin.ImRealnameListModel, totalCount int, err error)
|
||||
// View 获取IM实名审核指定信息
|
||||
View(ctx context.Context, in *sysin.ImRealnameViewInp) (res *sysin.ImRealnameViewModel, err error)
|
||||
// Review 审核IM实名
|
||||
Review(ctx context.Context, in *sysin.ImRealnameReviewInp) (err error)
|
||||
}
|
||||
ISysImWorkOrder interface {
|
||||
// List 获取IM举报反馈工单列表
|
||||
List(ctx context.Context, in *sysin.ImWorkOrderListInp) (list []*sysin.ImWorkOrderListModel, totalCount int, err error)
|
||||
// View 获取IM举报反馈工单指定信息
|
||||
View(ctx context.Context, in *sysin.ImWorkOrderViewInp) (res *sysin.ImWorkOrderViewModel, err error)
|
||||
// Status 更新IM举报反馈工单状态
|
||||
Status(ctx context.Context, in *sysin.ImWorkOrderStatusInp) (err error)
|
||||
}
|
||||
ISysImAuditLog interface {
|
||||
// List 获取IM审计日志列表
|
||||
List(ctx context.Context, in *sysin.ImAuditLogListInp) (list []*sysin.ImAuditLogListModel, totalCount int, err error)
|
||||
// View 获取IM审计日志指定信息
|
||||
View(ctx context.Context, in *sysin.ImAuditLogViewInp) (res *sysin.ImAuditLogViewModel, err error)
|
||||
}
|
||||
ISysDictData interface {
|
||||
// Delete 删除
|
||||
Delete(ctx context.Context, in *sysin.DictDataDeleteInp) error
|
||||
@@ -414,6 +498,14 @@ var (
|
||||
localSysCron ISysCron
|
||||
localSysCronGroup ISysCronGroup
|
||||
localSysCurdDemo ISysCurdDemo
|
||||
localSysImTenant ISysImTenant
|
||||
localSysImRegistration ISysImRegistration
|
||||
localSysImInvite ISysImInvite
|
||||
localSysImUser ISysImUser
|
||||
localSysImGroup ISysImGroup
|
||||
localSysImRealname ISysImRealname
|
||||
localSysImWorkOrder ISysImWorkOrder
|
||||
localSysImAuditLog ISysImAuditLog
|
||||
localSysDictData ISysDictData
|
||||
localSysDictType ISysDictType
|
||||
localSysEmsLog ISysEmsLog
|
||||
@@ -517,6 +609,94 @@ func RegisterSysCurdDemo(i ISysCurdDemo) {
|
||||
localSysCurdDemo = i
|
||||
}
|
||||
|
||||
func SysImTenant() ISysImTenant {
|
||||
if localSysImTenant == nil {
|
||||
panic("implement not found for interface ISysImTenant, forgot register?")
|
||||
}
|
||||
return localSysImTenant
|
||||
}
|
||||
|
||||
func RegisterSysImTenant(i ISysImTenant) {
|
||||
localSysImTenant = i
|
||||
}
|
||||
|
||||
func SysImRegistration() ISysImRegistration {
|
||||
if localSysImRegistration == nil {
|
||||
panic("implement not found for interface ISysImRegistration, forgot register?")
|
||||
}
|
||||
return localSysImRegistration
|
||||
}
|
||||
|
||||
func RegisterSysImRegistration(i ISysImRegistration) {
|
||||
localSysImRegistration = i
|
||||
}
|
||||
|
||||
func SysImInvite() ISysImInvite {
|
||||
if localSysImInvite == nil {
|
||||
panic("implement not found for interface ISysImInvite, forgot register?")
|
||||
}
|
||||
return localSysImInvite
|
||||
}
|
||||
|
||||
func RegisterSysImInvite(i ISysImInvite) {
|
||||
localSysImInvite = i
|
||||
}
|
||||
|
||||
func SysImUser() ISysImUser {
|
||||
if localSysImUser == nil {
|
||||
panic("implement not found for interface ISysImUser, forgot register?")
|
||||
}
|
||||
return localSysImUser
|
||||
}
|
||||
|
||||
func RegisterSysImUser(i ISysImUser) {
|
||||
localSysImUser = i
|
||||
}
|
||||
|
||||
func SysImGroup() ISysImGroup {
|
||||
if localSysImGroup == nil {
|
||||
panic("implement not found for interface ISysImGroup, forgot register?")
|
||||
}
|
||||
return localSysImGroup
|
||||
}
|
||||
|
||||
func RegisterSysImGroup(i ISysImGroup) {
|
||||
localSysImGroup = i
|
||||
}
|
||||
|
||||
func SysImRealname() ISysImRealname {
|
||||
if localSysImRealname == nil {
|
||||
panic("implement not found for interface ISysImRealname, forgot register?")
|
||||
}
|
||||
return localSysImRealname
|
||||
}
|
||||
|
||||
func RegisterSysImRealname(i ISysImRealname) {
|
||||
localSysImRealname = i
|
||||
}
|
||||
|
||||
func SysImWorkOrder() ISysImWorkOrder {
|
||||
if localSysImWorkOrder == nil {
|
||||
panic("implement not found for interface ISysImWorkOrder, forgot register?")
|
||||
}
|
||||
return localSysImWorkOrder
|
||||
}
|
||||
|
||||
func RegisterSysImWorkOrder(i ISysImWorkOrder) {
|
||||
localSysImWorkOrder = i
|
||||
}
|
||||
|
||||
func SysImAuditLog() ISysImAuditLog {
|
||||
if localSysImAuditLog == nil {
|
||||
panic("implement not found for interface ISysImAuditLog, forgot register?")
|
||||
}
|
||||
return localSysImAuditLog
|
||||
}
|
||||
|
||||
func RegisterSysImAuditLog(i ISysImAuditLog) {
|
||||
localSysImAuditLog = i
|
||||
}
|
||||
|
||||
func SysDictData() ISysDictData {
|
||||
if localSysDictData == nil {
|
||||
panic("implement not found for interface ISysDictData, forgot register?")
|
||||
|
||||
@@ -7,6 +7,7 @@ package main
|
||||
|
||||
import (
|
||||
_ "hotgo/internal/packed"
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
|
||||
|
||||
@@ -237,6 +237,9 @@ database:
|
||||
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
debug: true
|
||||
Prefix: "hg_"
|
||||
imRegistry:
|
||||
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
|
||||
debug: true
|
||||
|
||||
|
||||
# 链路追踪
|
||||
@@ -300,4 +303,8 @@ hggen:
|
||||
addon:
|
||||
srcPath: "./resource/generate/default/addon" # 生成模板路径
|
||||
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
|
||||
|
||||
imDirectDb:
|
||||
# Each HotGo writer must use a unique ID which does not overlap an IM node.
|
||||
snowflakeWorkerId: 1023
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM alpine:3.20
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
COPY temp/linux_arm64/hotgo /app/hotgo
|
||||
COPY hack /app/hack
|
||||
COPY manifest/config /app/manifest/config
|
||||
COPY resource /app/resource
|
||||
COPY storage /app/storage
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["/app/hotgo"]
|
||||
@@ -0,0 +1,29 @@
|
||||
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 @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' 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, '审计日志', 'imAuditLog', 'audit-log', '', '1', '/im/audit-log/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '60', '', '1', @now, @now
|
||||
WHERE @auditDirId IS NULL;
|
||||
|
||||
SELECT @auditDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLog' LIMIT 1;
|
||||
|
||||
SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' 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, @auditDirId, '审计列表', 'imAuditLogIndex', 'index', '', '2', '', '/imAuditLog/list', '', '/imAuditLog/index', '1', 'imAuditLog', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @auditListId IS NULL;
|
||||
|
||||
SELECT @auditListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imAuditLogIndex' 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, @auditListId, '审计详情', 'imAuditLogView', '', '', '3', '', '/imAuditLog/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @auditDirId, ' tr_', @auditListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imAuditLogView');
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS im_audit_logs (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
actor_uid BIGINT NOT NULL DEFAULT 0,
|
||||
actor_role VARCHAR(64) NOT NULL DEFAULT '',
|
||||
action VARCHAR(128) NOT NULL,
|
||||
target_type VARCHAR(64) NOT NULL DEFAULT '',
|
||||
target_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
before_snapshot JSON,
|
||||
after_snapshot JSON,
|
||||
reason VARCHAR(512) NOT NULL DEFAULT '',
|
||||
request_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
user_agent VARCHAR(512) NOT NULL DEFAULT '',
|
||||
metadata JSON,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_im_audit_logs_actor (actor_uid, id DESC),
|
||||
INDEX idx_im_audit_logs_action (action, id DESC),
|
||||
INDEX idx_im_audit_logs_target (target_type, target_id, id DESC),
|
||||
INDEX idx_im_audit_logs_request (request_id),
|
||||
INDEX idx_im_audit_logs_created (created_at, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='IM租户审计日志';
|
||||
@@ -0,0 +1,57 @@
|
||||
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 @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' 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, '群组管理', 'imGroup', 'group', '', '1', '/im/group/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '30', '', '1', @now, @now
|
||||
WHERE @groupDirId IS NULL;
|
||||
|
||||
SELECT @groupDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroup' LIMIT 1;
|
||||
|
||||
SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' 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, @groupDirId, '群组列表', 'imGroupIndex', 'index', '', '2', '', '/imGroup/list', '', '/imGroup/index', '1', 'imGroup', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @groupListId IS NULL;
|
||||
|
||||
SELECT @groupListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imGroupIndex' 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, @groupListId, '群组详情', 'imGroupView', '', '', '3', '', '/imGroup/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupView');
|
||||
|
||||
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, @groupListId, '群成员列表', 'imGroupMemberList', '', '', '3', '', '/imGroup/memberList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMemberList');
|
||||
|
||||
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, @groupListId, '群治理配置', 'imGroupGovernance', '', '', '3', '', '/imGroup/governance', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '30', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupGovernance');
|
||||
|
||||
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, @groupListId, '入群申请列表', 'imGroupJoinRequestList', '', '', '3', '', '/imGroup/joinRequestList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '40', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupJoinRequestList');
|
||||
|
||||
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, @groupListId, '群邀请列表', 'imGroupInvitationList', '', '', '3', '', '/imGroup/invitationList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '50', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupInvitationList');
|
||||
|
||||
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, @groupListId, '群公告列表', 'imGroupAnnouncementList', '', '', '3', '', '/imGroup/announcementList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '60', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAnnouncementList');
|
||||
|
||||
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, @groupListId, '群禁言列表', 'imGroupMuteList', '', '', '3', '', '/imGroup/muteList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '70', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupMuteList');
|
||||
|
||||
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, @groupListId, '群管理日志', 'imGroupAdminLogList', '', '', '3', '', '/imGroup/adminLogList', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @groupDirId, ' tr_', @groupListId, ' '), '80', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imGroupAdminLogList');
|
||||
@@ -0,0 +1,33 @@
|
||||
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 @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' 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, '实名审核', 'imRealname', 'realname', '', '1', '/im/realname/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '40', '', '1', @now, @now
|
||||
WHERE @realnameDirId IS NULL;
|
||||
|
||||
SELECT @realnameDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealname' LIMIT 1;
|
||||
|
||||
SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' 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, @realnameDirId, '审核列表', 'imRealnameIndex', 'index', '', '2', '', '/imRealname/list', '', '/imRealname/index', '1', 'imRealname', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @realnameListId IS NULL;
|
||||
|
||||
SELECT @realnameListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imRealnameIndex' 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, @realnameListId, '实名详情', 'imRealnameView', '', '', '3', '', '/imRealname/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameView');
|
||||
|
||||
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, @realnameListId, '实名审核操作', 'imRealnameReview', '', '', '3', '', '/imRealname/review', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @realnameDirId, ' tr_', @realnameListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imRealnameReview');
|
||||
@@ -0,0 +1,37 @@
|
||||
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 @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' 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, '租户管理', 'imTenant', 'tenant', '', '1', '/im/tenant/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @tenantDirId IS NULL;
|
||||
|
||||
SELECT @tenantDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenant' LIMIT 1;
|
||||
|
||||
SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' 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, @tenantDirId, '租户列表', 'imTenantIndex', 'index', '', '2', '', '/imTenant/list', '', '/imTenant/index', '1', 'imTenant', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @tenantListId IS NULL;
|
||||
|
||||
SELECT @tenantListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imTenantIndex' 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, @tenantListId, '租户详情', 'imTenantView', '', '', '3', '', '/imTenant/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantView');
|
||||
|
||||
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, @tenantListId, '编辑/新增租户', 'imTenantEdit', '', '', '3', '', '/imTenant/edit', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantEdit');
|
||||
|
||||
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, @tenantListId, '修改租户状态', 'imTenantStatus', '', '', '3', '', '/imTenant/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @tenantDirId, ' tr_', @tenantListId, ' '), '30', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imTenantStatus');
|
||||
@@ -0,0 +1,33 @@
|
||||
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 @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' 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, '用户管理', 'imUser', 'user', '', '1', '/im/user/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '20', '', '1', @now, @now
|
||||
WHERE @userDirId IS NULL;
|
||||
|
||||
SELECT @userDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUser' LIMIT 1;
|
||||
|
||||
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' 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, @userDirId, '用户列表', 'imUserIndex', 'index', '', '2', '', '/imUser/list', '', '/imUser/index', '1', 'imUser', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @userListId IS NULL;
|
||||
|
||||
SELECT @userListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imUserIndex' 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, @userListId, '用户详情', 'imUserView', '', '', '3', '', '/imUser/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserView');
|
||||
|
||||
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, @userListId, '重置用户密码', 'imUserResetPassword', '', '', '3', '', '/imUser/resetPassword', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @userDirId, ' tr_', @userListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imUserResetPassword');
|
||||
@@ -0,0 +1,33 @@
|
||||
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 @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' 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, '举报反馈', 'imWorkOrder', 'work-order', '', '1', '/im/work-order/index', '', '', 'ParentLayout', '1', '', '0', '0', '', '0', '0', '0', '2', CONCAT('tr_', @imRootId, ' '), '50', '', '1', @now, @now
|
||||
WHERE @workOrderDirId IS NULL;
|
||||
|
||||
SELECT @workOrderDirId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrder' LIMIT 1;
|
||||
|
||||
SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' 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, @workOrderDirId, '工单列表', 'imWorkOrderIndex', 'index', '', '2', '', '/imWorkOrder/list', '', '/imWorkOrder/index', '1', 'imWorkOrder', '0', '0', '', '0', '0', '0', '3', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' '), '10', '', '1', @now, @now
|
||||
WHERE @workOrderListId IS NULL;
|
||||
|
||||
SELECT @workOrderListId := `id` FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderIndex' 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, @workOrderListId, '工单详情', 'imWorkOrderView', '', '', '3', '', '/imWorkOrder/view', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '10', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderView');
|
||||
|
||||
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, @workOrderListId, '更新工单状态', 'imWorkOrderStatus', '', '', '3', '', '/imWorkOrder/status', '', '', '1', '', '0', '0', '', '0', '1', '0', '4', CONCAT('tr_', @imRootId, ' tr_', @workOrderDirId, ' tr_', @workOrderListId, ' '), '20', '', '1', @now, @now
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `hg_admin_menu` WHERE `name` = 'imWorkOrderStatus');
|
||||
@@ -223,6 +223,7 @@ COMMENT ON COLUMN hg_admin_dept.updated_at IS '更新时间';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS hg_admin_member (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT,
|
||||
dept_id BIGINT DEFAULT 0,
|
||||
role_id BIGINT DEFAULT 10,
|
||||
real_name VARCHAR(32) DEFAULT '',
|
||||
|
||||
@@ -213,6 +213,7 @@ INSERT INTO `hg_admin_dept` (`id`, `pid`, `name`, `code`, `type`, `leader`, `pho
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `hg_admin_member` (
|
||||
`id` bigint(20) NOT NULL COMMENT '管理员ID',
|
||||
`tenant_id` bigint(20) DEFAULT NULL COMMENT 'IM租户ID',
|
||||
`dept_id` bigint(20) DEFAULT '0' COMMENT '部门ID',
|
||||
`role_id` bigint(20) DEFAULT '10' COMMENT '角色ID',
|
||||
`real_name` varchar(32) DEFAULT '' COMMENT '真实姓名',
|
||||
@@ -5789,6 +5790,7 @@ ALTER TABLE `hg_admin_dept`
|
||||
ALTER TABLE `hg_admin_member`
|
||||
ADD PRIMARY KEY (`id`),
|
||||
ADD UNIQUE KEY `invite_code` (`invite_code`),
|
||||
ADD KEY `idx_hg_admin_member_tenant` (`tenant_id`),
|
||||
ADD KEY `dept_id` (`dept_id`),
|
||||
ADD KEY `pid` (`pid`);
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
-- IM registration SMS switch and registration invite admin migration.
|
||||
-- This file is idempotent for MySQL deployments using HotGo's default hg_ prefix.
|
||||
|
||||
-- im_tenant is the only tenant registry. Admin identities reference it directly.
|
||||
SET @tenant_column_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND COLUMN_NAME = 'tenant_id'
|
||||
);
|
||||
SET @ddl := IF(
|
||||
@tenant_column_exists = 0,
|
||||
'ALTER TABLE hg_admin_member ADD COLUMN tenant_id BIGINT NULL COMMENT ''IM tenant ID'' AFTER id',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Preserve an existing deployment's old mapping before removing the redundant table.
|
||||
SET @binding_table_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_admin_tenant_binding'
|
||||
);
|
||||
SET @dml := IF(
|
||||
@binding_table_exists > 0,
|
||||
'UPDATE hg_admin_member AS m JOIN im_admin_tenant_binding AS b ON b.hotgo_tenant_id = m.id SET m.tenant_id = b.im_tenant_id WHERE b.status = 1 AND m.tenant_id IS NULL',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @dml;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
DROP TABLE IF EXISTS im_admin_tenant_binding;
|
||||
|
||||
SET @tenant_index_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND INDEX_NAME = 'idx_hg_admin_member_tenant'
|
||||
);
|
||||
SET @ddl := IF(
|
||||
@tenant_index_exists = 0,
|
||||
'ALTER TABLE hg_admin_member ADD INDEX idx_hg_admin_member_tenant (tenant_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @tenant_fk_exists := (
|
||||
SELECT COUNT(*) FROM information_schema.REFERENTIAL_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'hg_admin_member' AND CONSTRAINT_NAME = 'fk_hg_admin_member_im_tenant'
|
||||
);
|
||||
SET @ddl := IF(
|
||||
@tenant_fk_exists = 0,
|
||||
'ALTER TABLE hg_admin_member ADD CONSTRAINT fk_hg_admin_member_im_tenant FOREIGN KEY (tenant_id) REFERENCES im_tenant(id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_admin_operation_audit (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
tenant_id BIGINT NULL,
|
||||
operator_id BIGINT NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
target_type VARCHAR(32) NOT NULL,
|
||||
target_id BIGINT NULL,
|
||||
result VARCHAR(16) NOT NULL,
|
||||
ip VARCHAR(64) NOT NULL DEFAULT '',
|
||||
detail_json JSON NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_im_admin_audit_tenant_time (tenant_id, created_at),
|
||||
KEY idx_im_admin_audit_operator_time (operator_id, created_at),
|
||||
UNIQUE KEY uk_im_admin_audit_request_action (request_id, action)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Invitation definitions belong to the HotGo control database. Tenant
|
||||
-- databases retain only im_registration_invite_uses.
|
||||
CREATE TABLE IF NOT EXISTS im_registration_invite_batches (
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
quantity INT UNSIGNED NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
created_by BIGINT NOT NULL,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_registration_invite_batch_tenant_idempotency (tenant_id, idempotency_key),
|
||||
KEY idx_im_registration_invite_batch_tenant_created (tenant_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS im_registration_invite_codes (
|
||||
id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
batch_id BIGINT NULL,
|
||||
code CHAR(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
usage_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_used_at DATETIME(3) NULL,
|
||||
created_by BIGINT NOT NULL,
|
||||
remark VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL,
|
||||
updated_at DATETIME(3) NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_im_registration_invite_code_tenant (tenant_id, code),
|
||||
KEY idx_im_registration_invite_tenant_status_expires (tenant_id, status, expires_at),
|
||||
KEY idx_im_registration_invite_tenant_batch (tenant_id, batch_id),
|
||||
KEY idx_im_registration_invite_tenant_created (tenant_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO hg_sys_config
|
||||
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
|
||||
SELECT
|
||||
'im', '注册短信发送', 'bool', 'register_sms_enabled', '1', '1', 10,
|
||||
'控制 IM 注册验证码是否真实发送', 1, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM hg_sys_config WHERE `group` = 'im' AND `key` = 'register_sms_enabled'
|
||||
);
|
||||
|
||||
INSERT INTO hg_admin_menu
|
||||
(pid, level, tree, title, name, path, icon, type, redirect, permissions, component, always_show, is_root, is_frame, keep_alive, hidden, affix, sort, status, created_at, updated_at)
|
||||
SELECT
|
||||
0, 1, '', 'IM管理', 'imManager', '/im', 'AppstoreOutlined', 1, '', '', 'LAYOUT', 0, 0, 1, 0, 0, 0, 125, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imManager');
|
||||
|
||||
SET @imRoot := (SELECT id FROM hg_admin_menu WHERE name = 'imManager' LIMIT 1);
|
||||
UPDATE hg_admin_menu SET tree = CONCAT('tr_', id, ' '), component = 'LAYOUT' WHERE id = @imRoot;
|
||||
|
||||
INSERT INTO hg_admin_menu
|
||||
(pid, level, tree, title, name, path, icon, type, redirect, permissions, component, always_show, is_root, is_frame, keep_alive, hidden, affix, sort, status, created_at, updated_at)
|
||||
SELECT
|
||||
@imRoot, 2, '', '注册邀请码', 'imRegistrationInviteIndex', 'registrationInvite', '', 2, '', '/imInvite/list', '/im/registrationInvite/index', 0, 0, 1, 0, 0, 0, 70, 1, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imRegistrationInviteIndex');
|
||||
|
||||
SET @invitePage := (SELECT id FROM hg_admin_menu WHERE name = 'imRegistrationInviteIndex' LIMIT 1);
|
||||
UPDATE hg_admin_menu SET tree = CONCAT('tr_', id, ' tr_', @imRoot, ' ') WHERE id = @invitePage;
|
||||
|
||||
INSERT INTO hg_admin_menu
|
||||
(pid, level, tree, title, name, path, icon, type, permissions, component, sort, status, created_at, updated_at)
|
||||
SELECT @invitePage, 3, CONCAT('tr_', @invitePage, ' tr_', @imRoot, ' '), title, name, '', '', 3, permissions, '', sort, 1, NOW(), NOW()
|
||||
FROM (
|
||||
SELECT '生成邀请码' title, 'imInviteCreate' name, '/imInvite/create' permissions, 10 sort UNION ALL
|
||||
SELECT '批量生成邀请码', 'imInviteBatchCreate', '/imInvite/batchCreate', 20 UNION ALL
|
||||
SELECT '批次详情', 'imInviteBatchView', '/imInvite/batchView', 30 UNION ALL
|
||||
SELECT '撤销邀请码', 'imInviteRevoke', '/imInvite/revoke', 40 UNION ALL
|
||||
SELECT '批量撤销邀请码', 'imInviteBatchRevoke', '/imInvite/batchRevoke', 50 UNION ALL
|
||||
SELECT '导出邀请码', 'imInviteExport', '/imInvite/export', 60
|
||||
) p
|
||||
WHERE NOT EXISTS (SELECT 1 FROM hg_admin_menu m WHERE m.name = p.name);
|
||||
|
||||
SET @imUserPage := (SELECT id FROM hg_admin_menu WHERE name = 'imUserIndex' LIMIT 1);
|
||||
INSERT INTO hg_admin_menu
|
||||
(pid, level, tree, title, name, path, icon, type, permissions, component, hidden, sort, status, created_at, updated_at)
|
||||
SELECT @imUserPage, 4, CONCAT('tr_', @imUserPage, ' '), '重置用户密码', 'imUserResetPassword', '', '', 3,
|
||||
'/imUser/resetPassword', '', 1, 20, 1, NOW(), NOW()
|
||||
WHERE @imUserPage IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM hg_admin_menu WHERE name = 'imUserResetPassword');
|
||||
@@ -100,6 +100,7 @@ CREATE TABLE `hg_admin_dept` ( -- 管理员_部门
|
||||
);
|
||||
CREATE TABLE `hg_admin_member` ( -- 管理员_用户表
|
||||
`id` INTEGER NOT NULL PRIMARY KEY, -- 管理员ID
|
||||
`tenant_id` INTEGER DEFAULT NULL, -- IM租户ID
|
||||
`dept_id` INTEGER DEFAULT 0, -- 部门ID
|
||||
`role_id` INTEGER DEFAULT 10, -- 角色ID
|
||||
`real_name` TEXT DEFAULT '', -- 真实姓名
|
||||
@@ -653,4 +654,4 @@ CREATE INDEX `hg_sys_provinces_pid` ON `hg_sys_provinces` (`pid`);
|
||||
CREATE UNIQUE INDEX `hg_sys_serve_license_appid` ON `hg_sys_serve_license` (`appid`);
|
||||
CREATE INDEX `hg_sys_serve_log_member_id` ON `hg_sys_serve_log` (`level_format`);
|
||||
CREATE INDEX `hg_sys_serve_log_traceid` ON `hg_sys_serve_log` (`trace_id`);
|
||||
CREATE INDEX `hg_sys_sms_log_mobile` ON `hg_sys_sms_log` (`mobile`);
|
||||
CREATE INDEX `hg_sys_sms_log_mobile` ON `hg_sys_sms_log` (`mobile`);
|
||||
|
||||
Generated
+629
-322
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
allowBuilds:
|
||||
'@swc/core': set this to true or false
|
||||
core-js: set this to true or false
|
||||
esbuild: set this to true or false
|
||||
vue-demi: set this to true or false
|
||||
@@ -0,0 +1,77 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/list',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Create(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/create',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function BatchCreate(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/batchCreate',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function BatchView(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/batchView',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Revoke(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/revoke',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function BatchRevoke(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/batchRevoke',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Export(params) {
|
||||
return http.request({
|
||||
url: '/imInvite/export',
|
||||
method: 'POST',
|
||||
params,
|
||||
responseType: 'blob',
|
||||
requestOptions: {
|
||||
isTransformResponse: false,
|
||||
isReturnNativeResponse: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function GetRegistrationConfig() {
|
||||
return http.request({
|
||||
url: '/imRegistration/config/get',
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
export function UpdateRegistrationConfig(params) {
|
||||
return http.request({
|
||||
url: '/imRegistration/config/update',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM审计日志列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imAuditLog/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM审计日志详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imAuditLog/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM群组列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群组详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群成员列表
|
||||
export function MemberList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/memberList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群治理配置
|
||||
export function Governance(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/governance',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM入群申请列表
|
||||
export function JoinRequestList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/joinRequestList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群邀请列表
|
||||
export function InvitationList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/invitationList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群公告列表
|
||||
export function AnnouncementList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/announcementList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群禁言列表
|
||||
export function MuteList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/muteList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM群管理日志列表
|
||||
export function AdminLogList(params) {
|
||||
return http.request({
|
||||
url: '/imGroup/adminLogList',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imRealname/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imRealname/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export function Review(params) {
|
||||
return http.request({
|
||||
url: '/imRealname/review',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM租户列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 添加/编辑IM租户
|
||||
export function Edit(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/edit',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 修改IM租户状态
|
||||
export function Status(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/status',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM租户指定详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imTenant/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM用户列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imUser/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM用户指定详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imUser/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 管理员重置IM用户密码
|
||||
export function ResetPassword(params) {
|
||||
return http.request({
|
||||
url: '/imUser/resetPassword',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { http } from '@/utils/http/axios';
|
||||
|
||||
// 获取IM举报反馈工单列表
|
||||
export function List(params) {
|
||||
return http.request({
|
||||
url: '/imWorkOrder/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取IM举报反馈工单详情
|
||||
export function View(params) {
|
||||
return http.request({
|
||||
url: '/imWorkOrder/view',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新IM举报反馈工单状态
|
||||
export function Status(params) {
|
||||
return http.request({
|
||||
url: '/imWorkOrder/status',
|
||||
method: 'POST',
|
||||
params,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="card" title="批次详情" class="batch-view-modal">
|
||||
<n-descriptions :column="4" bordered size="small">
|
||||
<n-descriptions-item label="批次ID">{{ detail.batchId || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="总数">{{ detail.quantity || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="有效">{{ detail.activeCount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="累计注册次数">{{ detail.usageCount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="已撤销">{{ detail.revokedCount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="已过期">{{ detail.expiredCount || 0 }}</n-descriptions-item>
|
||||
<n-descriptions-item label="到期时间">{{ detail.expiresAt || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ detail.createdAt || '-' }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-data-table :columns="columns" :data="detail.list || []" :pagination="{ pageSize: 10 }" />
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BatchView } from '@/api/im/registrationInvite';
|
||||
|
||||
const message = useMessage();
|
||||
const showModal = ref(false);
|
||||
const detail = ref<any>({});
|
||||
const columns = [
|
||||
{ title: 'ID', key: 'id' },
|
||||
{ title: '邀请码', key: 'code' },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '累计使用次数', key: 'usageCount' },
|
||||
{ title: '最后使用时间', key: 'lastUsedAt' },
|
||||
];
|
||||
|
||||
async function openModal(tenantId: number, batchId: number) {
|
||||
showModal.value = true;
|
||||
try {
|
||||
detail.value = await BatchView({ tenantId, batchId });
|
||||
} catch (error: any) {
|
||||
message.error(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.batch-view-modal {
|
||||
width: min(1000px, 92vw);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<n-modal v-model:show="showModal" preset="dialog" :title="isBatch ? '批量生成邀请码' : '生成邀请码'">
|
||||
<n-form ref="formRef" :model="formValue" :rules="rules" label-placement="left" :label-width="90">
|
||||
<n-form-item v-if="isBatch" label="生成数量" path="quantity">
|
||||
<n-input-number v-model:value="formValue.quantity" :min="1" :max="1000" />
|
||||
</n-form-item>
|
||||
<n-form-item label="到期时间" path="expiresAt">
|
||||
<n-date-picker v-model:value="formValue.expiresAt" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="备注" path="remark">
|
||||
<n-input v-model:value="formValue.remark" type="textarea" maxlength="255" show-count />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button @click="closeModal">取消</n-button>
|
||||
<n-button type="primary" :loading="loading" @click="submit">确认</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BatchCreate, Create } from '@/api/im/registrationInvite';
|
||||
import { localRfc3339 } from './model';
|
||||
|
||||
const emit = defineEmits(['reload']);
|
||||
const message = useMessage();
|
||||
const showModal = ref(false);
|
||||
const loading = ref(false);
|
||||
const formRef = ref();
|
||||
const tenantId = ref(0);
|
||||
const isBatch = ref(false);
|
||||
const formValue = reactive({
|
||||
quantity: 100,
|
||||
expiresAt: Date.now() + 7 * 24 * 3600 * 1000,
|
||||
remark: '',
|
||||
idempotencyKey: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
quantity: { required: true, type: 'number', min: 1, max: 1000, message: '数量必须在1到1000之间' },
|
||||
expiresAt: { required: true, type: 'number', message: '请选择到期时间' },
|
||||
};
|
||||
|
||||
function openModal(id: number, batch = false) {
|
||||
tenantId.value = id;
|
||||
isBatch.value = batch;
|
||||
formValue.quantity = 100;
|
||||
formValue.expiresAt = Date.now() + 7 * 24 * 3600 * 1000;
|
||||
formValue.remark = '';
|
||||
formValue.idempotencyKey = crypto.randomUUID();
|
||||
showModal.value = true;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
showModal.value = false;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
await formRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
tenantId: tenantId.value,
|
||||
expiresAt: localRfc3339(formValue.expiresAt),
|
||||
remark: formValue.remark,
|
||||
idempotencyKey: formValue.idempotencyKey,
|
||||
};
|
||||
if (isBatch.value) {
|
||||
await BatchCreate({ ...params, quantity: formValue.quantity });
|
||||
} else {
|
||||
await Create(params);
|
||||
}
|
||||
message.success('生成成功');
|
||||
closeModal();
|
||||
emit('reload');
|
||||
} catch (error: any) {
|
||||
message.error(error.toString());
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ openModal });
|
||||
</script>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="IM注册邀请码"></n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm
|
||||
ref="searchFormRef"
|
||||
@register="register"
|
||||
@submit="reloadTable"
|
||||
@reset="reloadTable"
|
||||
@keyup.enter="reloadTable"
|
||||
/>
|
||||
<BasicTable
|
||||
ref="actionRef"
|
||||
:columns="tableColumns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
:actionColumn="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:resizeHeightOffset="-10000"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleCreate(false)" v-if="hasPermission(['/imInvite/create'])">
|
||||
添加
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleCreate(true)" v-if="hasPermission(['/imInvite/batchCreate'])">
|
||||
批量生成
|
||||
</n-button>
|
||||
<n-button @click="handleExport" v-if="hasPermission(['/imInvite/export'])">导出</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<CreateModal ref="createRef" @reload="reloadTable" />
|
||||
<BatchViewModal ref="batchViewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { NTag, useDialog, useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form/index';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { Export, List, Revoke } from '@/api/im/registrationInvite';
|
||||
import {
|
||||
columns,
|
||||
loadOptions,
|
||||
loadTenantOptions,
|
||||
schemas,
|
||||
State,
|
||||
statusLabel,
|
||||
statusOptions,
|
||||
tenantOptions,
|
||||
} from './model';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import CreateModal from './CreateModal.vue';
|
||||
import BatchViewModal from './BatchViewModal.vue';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const { hasPermission } = usePermission();
|
||||
const userStore = useUserStore();
|
||||
const actionRef = ref();
|
||||
const searchFormRef = ref<any>({});
|
||||
const createRef = ref();
|
||||
const batchViewRef = ref();
|
||||
const isPlatformAdmin = computed(() => userStore.isCompanyDept);
|
||||
|
||||
const tableColumns = computed(() =>
|
||||
columns.map((item: any) => {
|
||||
if (item.key !== 'status') {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
render(row: State) {
|
||||
const option: any = statusOptions.find((status) => status.value === row.status);
|
||||
return h(NTag as any, { type: option?.type || 'default', bordered: false }, () =>
|
||||
statusLabel(row.status)
|
||||
);
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 220,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record: State) {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '批次',
|
||||
onClick: handleBatchView.bind(null, record),
|
||||
auth: ['/imInvite/batchView'],
|
||||
ifShow: () => !!record.batchId,
|
||||
},
|
||||
{
|
||||
label: '撤销',
|
||||
type: 'error',
|
||||
onClick: handleRevoke.bind(null, record),
|
||||
auth: ['/imInvite/revoke'],
|
||||
ifShow: () => record.status === 'active',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
const [register, { setFieldsValue }] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
|
||||
const loadDataTable = async (res) => {
|
||||
const params = { ...searchFormRef.value?.formModel, ...res };
|
||||
return await List(params);
|
||||
};
|
||||
|
||||
function reloadTable() {
|
||||
actionRef.value?.reload();
|
||||
}
|
||||
|
||||
function selectedTenantId() {
|
||||
if (!isPlatformAdmin.value) {
|
||||
return 0;
|
||||
}
|
||||
return Number(searchFormRef.value?.formModel?.tenantId || 0);
|
||||
}
|
||||
|
||||
function handleCreate(batch: boolean) {
|
||||
const tenantId = selectedTenantId();
|
||||
if (isPlatformAdmin.value && !tenantId) {
|
||||
message.warning('请先选择租户');
|
||||
return;
|
||||
}
|
||||
createRef.value.openModal(tenantId, batch);
|
||||
}
|
||||
|
||||
function handleBatchView(record: State) {
|
||||
batchViewRef.value.openModal(Number(record.tenantId || selectedTenantId()), record.batchId);
|
||||
}
|
||||
|
||||
function handleRevoke(record: State) {
|
||||
dialog.warning({
|
||||
title: '确认撤销',
|
||||
content: '撤销后该邀请码不能继续使用,是否确认?',
|
||||
positiveText: '确认',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Revoke({ tenantId: Number(record.tenantId || selectedTenantId()), id: record.id });
|
||||
message.success('撤销成功');
|
||||
reloadTable();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const params = { ...searchFormRef.value?.formModel };
|
||||
const response: any = await Export(params);
|
||||
const blob = response?.data || response;
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `registration-invite-codes-${Date.now()}.csv`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loadOptions();
|
||||
if (isPlatformAdmin.value) {
|
||||
await loadTenantOptions();
|
||||
await setFieldsValue({ tenantId: null });
|
||||
if (tenantOptions.value.length === 0) {
|
||||
message.warning('暂无可用租户');
|
||||
}
|
||||
}
|
||||
reloadTable();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user