research.md 6.8 KB

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.HttpClientsHttpPost),封装为 @ComponentImClient,参考 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-urlim.ext-tokenim.create-pathim.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-admincom.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)。