# Research: 骑手与用户 IM 即时沟通账号接入 **Phase**: 0 | **Date**: 2026-06-23 本文件记录实现该功能所需的关键技术决策、调研结论与备选方案。所有 NEEDS CLARIFICATION 已在 spec 阶段通过用户澄清解决,此处补充实现层面的取舍依据。 ## 决策 1:开通触发机制 —— APP 主动调用独立接口 **Decision**: 不在用户注册流程内嵌 IM 开通;新增独立的 `POST /infouser/user/im/open` 接口,由 APP 在注册完成后(或首聊前)自行调用。 **Rationale**: 用户明确要求"不改动用户创建过程"。独立接口解耦了注册与 IM 开通——注册保持稳定不被外部依赖(IM 平台)拖累;开通失败可由 APP 自由重试,不影响用户已注册状态。同时该接口天然服务存量用户补建(老用户首聊时 APP 调同一接口即可)。 **Alternatives considered**: - 在 `insertInfoUser`/`saveOrUpdate` 注册成功后自动调用 IM:被用户否决(要求不改注册流程),且会让注册强依赖外部 IM 平台可用性。 - 定时任务全量回填:运维成本高、实时性差,作为本期范围外的补充手段。 ## 决策 2:幂等策略 —— 凭证已存在则直接返回 **Decision**: `openImAccount(userId)` 先查用户;若 `imApiKey` 非空,直接返回现有凭证,不再调用 IM 平台。 **Rationale**: 防止 APP 重试/网络抖动导致同一用户重复调用 `extCreate` 产生多个 IM 账号。基于本地凭证判定,简单可靠,避免依赖 IM 平台侧的去重语义。 **Alternatives considered**: - 每次都重新创建覆盖:会丢失历史 IM 账号、可能产生孤儿账号,不可接受。 - 加分布式锁防并发:本期单机 + 本地判定已足够;若后续多实例可补充 Redis 锁,当前不过度设计。 ## 决策 3:imUserId 存储类型 —— BIGINT / Long **Decision**: `im_user_id` 数据库列用 `BIGINT`,Java 字段用 `Long`。 **Rationale**: IM 平台返回示例 `userId: 2069305587785445400` 为 19 位整数,超出 `INT`(约 21 亿)和 JS 安全整数范围。Java `Long` 最大 9223372036854775807(19 位),可容纳。**注意**:JSON 序列化返回给 APP 时,19 位 Long 必须序列化为**字符串**,否则前端 JS 会精度丢失。本项目 `JwtUtil.setToken` 已有将 Long 转 String 的先例(`withClaim("id", String.valueOf(...))`);返回 VO 的 `imUserId` 字段建议用 `String` 类型或加 `@JsonSerialize` ToStringSerializer,本期采用 **VO 中 imUserId 用 String** 的稳妥做法。 **Alternatives considered**: - 数据库存 VARCHAR:丧失数值语义且无必要,BIGINT 更合理。 - 返回数字不加处理:前端 JS 会精度丢失(2069305587785445400 → 2069305587785445…),不可接受。 ## 决策 4:HTTP 客户端 —— org.apache.http(仿 NewebPay) **Decision**: 使用项目已有的 `org.apache.http.client.HttpClients`(`HttpPost`),封装为 `@Component` 类 `ImClient`,参考 `com.ruoyi.app.utils.newebpay.NewebPay`。 **Rationale**: 项目已统一使用 Apache HttpClient(见 `NewebPay.postFormRaw`),保持一致、无新依赖。IM `extCreate` 为 POST + Header 鉴权(非表单加密),实现比 NewebPay 更简单:构造 `HttpPost`,设 `extToken` 请求头,发送空体或最小体,解析 JSON 响应。 **Alternatives considered**: - RestTemplate / WebClient:项目未引入,新增技术栈成本,无必要。 - OkHttp:项目未用,不引入。 ## 决策 5:配置注入方式 —— @Value(仿 NewebPayPayController) **Decision**: 在 `ImClient` 内用 `@Value` 注入 `im.base-url`、`im.ext-token`、`im.create-path`、`im.timeout-ms`。 **Rationale**: 与 `NewebPayPayController` 的 `@Value("${newebpay.base-url}")` 模式一致。IM 配置为全局单例(一个域名 + 一个平台级 extToken),直接注入组件即可,无需 `@ConfigurationProperties` 类(仅 2~4 个字段,@Value 更轻量)。 **Alternatives considered**: - `@ConfigurationProperties` 绑定 POJO:字段少时反而啰嗦,本期不采用。 - 硬编码:被需求明确禁止(域名/令牌须配置化)。 ## 决策 6:失败处理 —— 不写库、抛 ServiceException **Decision**: IM 平台返回 `code != 200`、网络异常或超时时,`ImClient` 抛异常 → service 不写库 → controller 经全局异常处理返回失败 `AjaxResult`,APP 可重试。 **Rationale**: 保证不写入无效/部分凭证(如只拿到 userId 没 apiKey 的脏数据)。用户注册主流程不经过此接口,故失败仅影响开通本身,APP 重试即可。超时用 `RequestConfig.setConnectTimeout/SocketTimeout`(按配置 `im.timeout-ms`,默认 5000ms)控制。 ## 决策 7:用户身份识别 —— token 解析 userId **Decision**: 接口从 `request.getHeader("token")` 取 token,经 `new JwtUtil().getusid(token)` 解析 userId;为空则拒绝(返回未登录错误)。 **Rationale**: 与 `NewebPayPayController`(行 121-129)完全一致的鉴权模式,复用现有 JwtUtil,不引入新鉴权方式。不接受 APP 传 userId 参数,防越权为他人开通。 ## 决策 8:响应 VO —— ImAccountVo **Decision**: 新建 `ImAccountVo`,含 `apiKey`(String)、`imUserId`(String) 两字段,作为开通接口的 data 返回。 **Rationale**: APP 需要这两个值初始化 IM SDK。`imUserId` 用 String 规避前端精度问题(见决策 3)。 ## 决策 9:编排服务所处模块 —— ruoyi-admin(非 ruoyi-system) **Decision**: IM 开通编排服务 `ImAccountService` 放在 **ruoyi-admin**(`com.ruoyi.app.service`),而非原计划的 `ruoyi-system.InfoUserServiceImpl`。 **Rationale**: 编译期发现模块依赖方向约束——`ruoyi-admin → ruoyi-system`(反向不可)。`ImClient` 依赖 httpclient **4.x + fastjson2**(仅 ruoyi-admin 有,仿 `NewebPay`;ruoyi-system 仅有 httpclient5 + fastjson1)。若把 `ImClient` 放进 ruoyi-system 需重写为 HC5+fastjson1,风险更高。因此把 `ImClient` 与编排服务 `ImAccountService` 同置于 ruoyi-admin,编排层注入 `IInfoUserService`(system,admin 可见)做用户读写,复用 `WalletService` 同款模式(`com.ruoyi.app.service` 中已有服务注入 `IInfoUserService` 的先例)。 **Alternatives considered**: - `ImClient` 移入 ruoyi-system + 用 HC5/fastjson1 重写:技术债与重写风险高,弃。 - 编排逻辑塞进 Controller:Controller 过厚,弃;改为独立 `ImAccountService` 保持分层。 **对外契约不变**: `POST /infouser/user/im/open` 仍在 `InfoUserController`,请求/响应/行为与 `contracts/open-im-account.md` 完全一致,仅内部模块归属调整。 ## 结论 所有实现层面决策已明确,无遗留 NEEDS CLARIFICATION。可进入 Phase 1(data-model / contracts / quickstart)。