diff --git a/docs/im/IM-HotGo注册短信开关与邀请码管理改造交接文档.md b/docs/im/IM-HotGo注册短信开关与邀请码管理改造交接文档.md new file mode 100644 index 0000000..49b53bd --- /dev/null +++ b/docs/im/IM-HotGo注册短信开关与邀请码管理改造交接文档.md @@ -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 当前工作区已有改动,并保留与本任务无关的用户修改。 +``` diff --git a/docs/im/IM-未实现后续增强项.md b/docs/im/IM-未实现后续增强项.md new file mode 100644 index 0000000..b58999b --- /dev/null +++ b/docs/im/IM-未实现后续增强项.md @@ -0,0 +1,298 @@ +# IM 第一至十一章后续增强项 + +> 日期:2026-08-13 +> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈 +> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。 + +## 1. 第一章:登录、注册与安全 + +### 1.1 当前已完成基线 + +1. 企业码解析、租户状态校验和公开租户配置返回。 +2. 注册、登录前验证码发送与验证。 +3. 注册验证码 Token 消费,避免绕过验证码直接注册。 +4. 忘记密码挑战验证与重置确认。 +5. Token 绑定 `auth_version`,密码重置后旧 Token 失效。 +6. 登录后敏感操作二次验证挑战。 +7. 实名认证状态查询与提交,证件信息脱敏/保护存储。 +8. 独立密码设置与验证,包含弱密码规则和失败锁定。 +9. 登录后 bootstrap 返回实名、安全与菜单基础能力。 + +### 1.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | HTTP `Idempotency-Key` 统一存储与请求摘要冲突检测 | 当前部分接口要求幂等键,但还没有统一的 `tenant_id + uid + method + route + key + body_hash` 处理层。 | 相同键相同 body 返回同一结果;相同键不同 body 返回冲突。 | +| P0 | 验证码真实发送通道 | 当前验证码生成、校验和限频链路已具备,短信/邮件供应商发送仍需接入真实服务。 | 验证码可通过供应商下发;供应商失败有可观测错误和重试策略。 | +| P0 | 验证码限频维度补强 | 需要按租户、手机号、IP、设备、场景组合限频,并落库或接入 Redis。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 | +| P0 | 安全事件通知联动 | 密码重置、实名提交/审核、独立密码变更、新设备登录等应写入系统通知。 | 操作成功后 `/v1/im/system-notifications` 能看到对应事件。 | +| P1 | 实名审核工作流 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过后状态变更。 | 审核通过后 `realname.status=verified`;驳回可再次提交。 | +| P1 | 独立密码用途扩展 | 当前按 `purpose` 存储,可继续接入青少年模式关闭、敏感设置修改等业务入口。 | 不同 `purpose` 相互隔离;错误次数和锁定互不串扰。 | +| P1 | 设备与会话管理 | 需要登录设备列表、主动踢出设备、Token 刷新/撤销审计。 | 修改密码或踢设备后对应 Token 立即不可用。 | +| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 | +| P2 | 风控与反诈策略接入 | 第一章安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险动作触发二次验证或反诈提示,低风险动作直通。 | + +## 2. 第二章:消息首页、全局导航与系统通知 + +### 2.1 当前已完成基线 + +1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限和 `message_policy`。 +2. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。 +3. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。 +4. 消息 Header 基础校验:`x-im-schema`、`type`、`x-im-client-mid`。 +5. 普通用户禁止发布 `sys` Topic,且禁止伪造 `Head.type=system`。 +6. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。 +7. `POST /v1/im/topics/{topic}/messages/{seq}/recall` 支持版本校验、权限校验和时限内撤回。 +8. `POST /v1/im/topics/{topic}/messages/{seq}/revisions` 支持文本消息编辑、版本递增和修订记录。 +9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read`。 + +### 2.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 编辑/撤回多端实时事件广播 | 当前 HTTP 更新 DB 并返回成功,后续需要向 Topic 在线成员广播编辑/撤回事件。 | A 端撤回/编辑后,B 端无需刷新即可收到事件并更新 UI。 | +| P0 | 编辑/撤回事件 Outbox | DB 更新和实时广播应通过事务事件或 Outbox 衔接,避免更新成功但事件丢失。 | 模拟广播失败后,后台任务可补发事件。 | +| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 | +| P0 | `Idempotency-Key` 接入编辑/撤回 | 编辑/撤回目前依赖乐观版本控制,后续需要同时支持 HTTP 幂等键。 | 客户端超时重试同一请求不会重复写 revision。 | +| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色决定是否展示具体成员。 | 普通成员/管理员在不同策略下看到不同粒度回执。 | +| P1 | 群回执 cursor 精确分页 | 当前 cursor 是基础位置分页,后续可升级为稳定游标,避免成员变动造成跳页。 | 成员新增/删除时分页不重复、不漏主要数据。 | +| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 | +| P1 | 系统通知可信生产通道 | 当前已提供通知存储和读取接口,后续需要统一服务端生产 API/事件消费者,禁止业务散落直写。 | 安全、群、组织等模块通过统一通道创建通知。 | +| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 | +| P1 | 会话搜索排序与类型过滤 | 当前按现有订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序等。 | 搜索结果与消息首页排序、筛选规则一致。 | +| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 | +| P2 | 消息 Header schema 版本演进 | 当前做基础格式/枚举校验,后续需要按 `x-im-schema` 版本维护兼容矩阵。 | 老客户端消息可兼容,新客户端非法字段被拒绝。 | + +## 3. 第三章:单聊关系、权限与会话上下文 + +### 3.1 当前已完成基线 + +1. 单聊会话上下文接口返回双方关系、发送能力、对端公开资料和 P2P Topic。 +2. P2P 发送路径接入好友关系校验,非好友、拉黑状态下禁止直接发送。 +3. 好友申请、通过、拒绝、删除好友与单聊关系状态联动。 +4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。 +5. 会话侧可根据 `can_send` 锁定输入框,并引导重新添加好友。 + +### 3.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 | +| P0 | 关系变更实时同步 | 好友通过、删除、拉黑、解除拉黑后,应向双方在线端推送关系和会话能力变化。 | A 拉黑 B 后,B 端输入框无需刷新自动锁定。 | +| P0 | 关系事件 Outbox | 关系 DB 状态变化与系统通知、会话事件、Push 需要事务级衔接。 | 模拟推送失败后后台可补发,不产生重复事件。 | +| P1 | Pending 会话体验增强 | 待处理好友申请对应的单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝,不需要跳转新朋友页。 | +| P1 | 黑名单历史消息策略 | 当前发送/通话阻断已具备,后续需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 | +| P1 | 非好友资料隐私裁剪 | 单聊上下文中的对端资料需要接入隐私设置、企业可见性和青少年模式。 | 非好友只能看到允许公开字段;好友看到扩展字段。 | +| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 | +| P2 | 关系状态迁移测试矩阵 | 需要覆盖 none、pending_inbound、pending_outbound、friend、blocked_by_me、blocked_by_peer 的全迁移。 | 任意状态迁移后 DB、HTTP 上下文、WS 权限一致。 | + +## 4. 第四章:群消息、群成员与 @ 能力 + +### 4.1 当前已完成基线 + +1. 群成员列表接口支持基础分页、搜索和 `mentionable` 过滤。 +2. 返回成员角色、公开资料、ACL 和是否可被 @。 +3. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。 +4. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。 +5. 群消息读取链路保留 @ 元数据,客户端可据此展示提醒。 + +### 4.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在在线、离线、多端场景均收到一次提醒。 | +| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频,避免刷屏。 | 超限返回稳定错误码,管理员可配置限额。 | +| P0 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 | +| P1 | 群成员资料隐私裁剪 | 群内非好友主页需要按群关系、好友关系、隐私设置裁剪资料字段。 | 同一成员从群内和好友主页进入时字段符合策略差异。 | +| P1 | 群昵称/角色变更事件 | 群昵称、角色、禁言、移除等变化需要实时推送给群成员并刷新成员列表缓存。 | 群主修改成员角色后在线端成员列表自动更新。 | +| P1 | @ 候选排序优化 | @ 面板候选应支持拼音首字母、最近互动、角色优先和本地缓存协同。 | 大群搜索响应稳定,候选排序符合产品规则。 | +| P1 | 群治理策略配置化 | 成员私聊、成员邀请、全员禁言、上传权限等应统一由群治理配置控制。 | 不同群可配置不同治理能力,HTTP 与 WS 权限一致。 | +| P2 | 大群性能压测 | 需要覆盖 500、2000、10000 成员下的成员列表、@ 搜索和发送性能。 | 达到目标延迟和资源占用阈值。 | + +## 5. 第五章:通讯录、好友关系与黑名单 + +### 5.1 当前已完成基线 + +1. 好友列表支持查询、首字母、分页,返回备注、头像、显示名来源和会话 Topic。 +2. 新朋友列表、好友申请、通过、拒绝链路已具备。 +3. 用户搜索支持账号、精准账号和手机号类型,返回公开资料、关系和 `action`。 +4. 添加好友二维码支持短期签名 Token 生成与解析。 +5. 好友个人主页返回关系、资料和操作能力。 +6. 群内非好友个人主页返回群角色、关系、资料和操作能力。 +7. 好友备注更新、删除好友、拉黑/解除拉黑已具备。 +8. 黑名单列表和批量移出黑名单已具备。 + +### 5.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 好友/黑名单操作统一幂等 | 拉黑、解除、删除、批量移出、好友申请等需要统一接入 `Idempotency-Key` 存储和请求摘要。 | 客户端超时重试不会重复创建申请或重复触发事件。 | +| P0 | 好友关系实时事件 | 申请、通过、拒绝、删除、备注、拉黑等状态变化需要向相关在线端实时同步。 | 两端关系页、会话页和通讯录无需刷新自动更新。 | +| P0 | 用户搜索隐私策略 | 当前搜索基线可用,后续需要接入 `allow_user_search`、添加方式、手机号可搜索等租户和个人隐私设置。 | 禁止手机号添加的用户无法通过手机号被搜索到,且不泄露存在性。 | +| P0 | 二维码吊销与一次性策略 | 当前二维码是短期签名 Token,后续如需主动失效、刷新覆盖、扫码审计,需要落库或接入 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 | +| P1 | 好友资料字段精细裁剪 | 个人主页需按好友、非好友、群内关系、黑名单、实名/青少年等规则返回不同字段。 | 不同入口返回字段符合隐私矩阵。 | +| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致,中文首字母稳定。 | +| P1 | 黑名单占位资料 | 资料删除、离职或不可见时,黑名单列表仍需返回可解除的受控占位用户。 | 黑名单中失效用户可展示占位并成功移出。 | +| P1 | 好友申请防骚扰 | 需要按目标用户、申请人、IP、设备、来源和群来源限频,并支持拒收策略。 | 高频申请被限制;群来源申请校验双方仍在群内。 | +| P1 | 关系审计和安全通知 | 添加、删除、拉黑、解除、二维码解析等应写审计并按策略生成系统通知。 | 用户可在通知中看到关键关系事件,后台可追溯。 | +| P2 | 批量接口部分失败语义增强 | 批量移出黑名单目前返回逐项结果,后续需要统一批量接口错误码、重试建议和幂等结果缓存。 | 部分失败时客户端能只重试失败项。 | + +## 6. 第六章:群聊创建、资料维护与成员治理 + +### 6.1 当前已完成基线 + +1. 群列表、创建群、群资料查询与更新、群头像和群简介保存接口已具备。 +2. 群设置、个人群设置、退出群、群昵称接口已具备。 +3. 群二维码生成与扫码解析支持 `join_group` 场景。 +4. 群邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。 +5. 群公告最新查询、发布/更新、详情和已读接口已具备。 +6. 群治理配置 `GET/PATCH /governance` 已具备。 +7. 管理员候选、管理员上限、批量添加管理员、删除管理员和群主转让接口已具备。 +8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。 +9. 当前实现优先复用 Topic、Subscription 和 Topic Aux,把产品主链路接口跑通。 + +### 6.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 群业务独立表拆分 | 当前群公告、进群申请、治理配置、禁言记录先存 Topic Aux,后续需要拆为独立表。 | 公告、申请、治理、禁言均有 tenant/topic 维度索引,可稳定分页和审计。 | +| P0 | 群治理强事务状态机 | 邀请、申请审批、管理员变更、群主转让、禁言需在事务内同步业务表、订阅 ACL、成员数和事件。 | 并发审批/转让/禁言不会出现 ACL 与业务状态不一致。 | +| P0 | 第六章幂等键落库 | 邀请、申请处理、公告发布、治理 PATCH、管理员批量、群主转让、禁言批量都需要统一幂等。 | 超时重试同一请求返回同一结果,不重复加人、升权或发事件。 | +| P0 | 系统事件、Push 与 Outbox | 第六章所有成员/公告/治理变化需要生成可信群系统事件,并通过 Outbox 保证推送。 | 在线端实时刷新;模拟推送失败后可补偿且不重复。 | +| P1 | 群公告已读独立记录 | 当前公告已读返回基础结果,后续需记录每个成员的公告已读时间。 | 新公告只提醒未读成员,已读状态跨端同步。 | +| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 | +| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 | +| P1 | 禁言发送链路强校验 | 当前禁言接口已具备,后续需要在 WebSocket 发消息路径校验 `GROUP_MUTED`。 | 被禁言成员发群消息被服务端拒绝,过期后自动恢复。 | +| P1 | 群文件索引 | 6.27-6.28 需要独立群文件表,绑定上传文件、群成员权限、下载状态和删除权限。 | 群文件列表可搜索/分页,退群后不可下载旧 URL。 | +| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 | +| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 | + +## 7. 第七章:文件下载、收藏管理与聊天记录检索 + +### 7.1 当前已完成基线 + +1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。 +2. 收藏批量创建和基础分享前的数据结构已具备。 +3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。 +4. 消息上下文定位接口可围绕指定 seq 返回前后消息。 +5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。 + +### 7.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 收藏幂等与去重策略 | 收藏同一消息、同一文件或同一链接需要统一去重和幂等键处理。 | 重复点击收藏不会产生重复记录;超时重试返回同一收藏 ID。 | +| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 | +| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 | +| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 | +| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 | +| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 | +| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 | +| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 | +| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 | +| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 | + +## 8. 第八章:实时语音与视频通话 + +### 8.1 当前已完成基线 + +1. 通话 invite、accept、offer、answer、ice-candidate、hang-up 等事件沿用 Tinode call event 链路。 +2. 通话记录表 `im_call_records` 已具备,记录 `call_id`、Topic、seq、主叫/被叫、类型、状态、时长和结束原因。 +3. 通话记录创建、接听和结束状态更新已接入 store/adapter。 +4. 通话记录查询接口已具备:全局 `GET /v1/im/calls` 与 Topic 内 `GET /v1/im/topics/{topic}/calls`。 +5. 查询结果返回对端用户、方向、通话类型、状态、时长和关键时间点,客户端可先完成通话记录页对接。 + +### 8.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 | +| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 | +| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 | +| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 | +| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 | +| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 | +| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 | +| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 | +| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 | + +## 9. 第九章:企业工作台与办公协同 + +### 9.1 当前已完成基线 + +1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。 +2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。 +3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。 +4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。 +5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。 +6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。 +7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。 +8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。 + +### 9.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 | +| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 | +| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 | +| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 | +| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 | +| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 | +| P1 | 会议通知实时提醒 | 当前接口和表已具备,后续需要会议创建、更新、取消、响应后的在线事件和 Push。 | 参与人在线端无需刷新即可收到会议变化。 | +| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 | +| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 | +| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 | +| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 | + +## 10. 个人设置与客户端能力 + +### 10.1 当前已完成基线 + +1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。 +2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。 +3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。 +4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。 +5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。 + +### 10.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 | +| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 | +| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 | +| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 | +| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 | +| P1 | 资料隐私裁剪落地 | 隐私设置需要接入用户搜索、组织架构、会话资料页等读路径。 | 非授权用户无法通过其他接口绕过隐私配置。 | +| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 | + +## 11. 反诈安全、举报与意见反馈 + +### 11.1 当前已完成基线 + +1. URL 安全检查 `POST /v1/im/security/urls/check` 已接入风控 challenge 创建。 +2. 风控确认 `POST /v1/im/security/risk-challenges/{id}/confirm` 已具备,`continue` 返回短时 `risk_action_token`。 +3. 举报原因 `GET /v1/im/reports/abuse/reasons` 已具备,默认覆盖用户、群、消息目标。 +4. 举报提交 `POST /v1/im/reports/abuse` 已具备,支持幂等键、证据文件 ID、目标基础可见性校验和审核快照。 +5. 意见反馈配置 `GET /v1/im/feedback/config`、提交 `POST /v1/im/feedback` 已具备,支持附件规则、诊断授权和幂等键。 + +### 11.2 后续增强项 + +| 优先级 | 增强项 | 说明 | 建议验收点 | +| --- | --- | --- | --- | +| P0 | 风控策略引擎 | 当前 URL 检查仅基础规则,后续需要按动作、关系、内容、设备、频率和租户策略综合判定。 | 高风险发消息、加好友、打开链接等动作统一返回风控 challenge。 | +| P0 | 风控 token 消费接入业务接口 | 当前确认可签发 `risk_action_token`,后续需要原业务接口校验并单次消费该 token。 | 二次确认后只允许重试原动作一次,重复使用返回 Token 已消费。 | +| P0 | 举报目标完整权限校验 | 当前做基础可见性校验,后续需要结合用户侧删除、撤回、退群、黑名单、文件 ACL 和审核状态。 | 无权限目标无法提交举报,也不泄露目标存在性。 | +| P0 | 审核后台流转 | 举报和反馈当前仅 received,后续需要后台分派、处理、关闭、回复和通知。 | 处理状态可追踪,举报人按策略收到处理结果。 | +| P1 | 证据文件安全与保留期 | 证据文件需要 `abuse_evidence` 专用 ACL、病毒扫描、不可转发和保留期策略。 | 证据只能审核链路访问,过期按政策清理。 | +| P1 | 反诈系统事件可信生产 | 低风险提示需通过可信 `security.risk_notice` 系统事件发送,并限制普通消息伪装。 | 普通用户无法伪造反诈弹窗,客户端只信任系统事件。 | +| P1 | 举报合并与限频 | 重复举报需要合并、返回原受理号或限频错误,避免刷单。 | 短时间重复举报同一目标不会生成大量工单。 | +| P2 | 反馈诊断包脱敏 | 诊断包需统一脱敏、大小限制、客户端日志授权和服务端下载审计。 | 未同意时没有诊断包,同意后包内不含明文 token/手机号。 | + +## 12. 统一处理建议 + +1. 先做 P0:统一幂等、实时事件/Outbox、验证码真实发送与限频、P2P/好友/搜索隐私策略、第六章群业务独立表与事务状态机、文件下载签名、通话强状态机、工作台/组织权限。 +2. 再做 P1:租户策略配置、系统通知生产通道、实名审核、设备管理、资料隐私裁剪、关系审计、群禁言发送校验、群文件索引、通话质量指标、会议/日报/月报/信箱完整业务表。 +3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。 +4. 每个增强项落地前同步更新 `IM-系统设计文档.md` 和 `IM-前后端对接文档.md` 中对应章节状态。 diff --git a/server/go.mod b/server/go.mod index 09fb277..b483c1f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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,10 +35,12 @@ 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/xdg-go/scram v1.1.2 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 @@ -81,7 +84,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 @@ -150,7 +152,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 diff --git a/server/go.sum b/server/go.sum index f60306f..216392c 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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= diff --git a/server/internal/library/imtenant/snowflake.go b/server/internal/library/imtenant/snowflake.go new file mode 100644 index 0000000..7861315 --- /dev/null +++ b/server/internal/library/imtenant/snowflake.go @@ -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 +} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..791bef2 --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +allowBuilds: + '@swc/core': true + core-js: true + esbuild: true + vue-demi: true diff --git a/web/src/views/org/user/list.vue b/web/src/views/org/user/list.vue index fc4bf12..1d461f7 100644 --- a/web/src/views/org/user/list.vue +++ b/web/src/views/org/user/list.vue @@ -87,6 +87,17 @@ class="py-4" > + + + + +