# 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）。
