Phase: 0 | Date: 2026-06-23
本文件记录实现该功能所需的关键技术决策、调研结论与备选方案。所有 NEEDS CLARIFICATION 已在 spec 阶段通过用户澄清解决,此处补充实现层面的取舍依据。
Decision: 不在用户注册流程内嵌 IM 开通;新增独立的 POST /infouser/user/im/open 接口,由 APP 在注册完成后(或首聊前)自行调用。
Rationale: 用户明确要求"不改动用户创建过程"。独立接口解耦了注册与 IM 开通——注册保持稳定不被外部依赖(IM 平台)拖累;开通失败可由 APP 自由重试,不影响用户已注册状态。同时该接口天然服务存量用户补建(老用户首聊时 APP 调同一接口即可)。
Alternatives considered:
insertInfoUser/saveOrUpdate 注册成功后自动调用 IM:被用户否决(要求不改注册流程),且会让注册强依赖外部 IM 平台可用性。Decision: openImAccount(userId) 先查用户;若 imApiKey 非空,直接返回现有凭证,不再调用 IM 平台。
Rationale: 防止 APP 重试/网络抖动导致同一用户重复调用 extCreate 产生多个 IM 账号。基于本地凭证判定,简单可靠,避免依赖 IM 平台侧的去重语义。
Alternatives considered:
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:
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:
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:字段少时反而啰嗦,本期不采用。Decision: IM 平台返回 code != 200、网络异常或超时时,ImClient 抛异常 → service 不写库 → controller 经全局异常处理返回失败 AjaxResult,APP 可重试。
Rationale: 保证不写入无效/部分凭证(如只拿到 userId 没 apiKey 的脏数据)。用户注册主流程不经过此接口,故失败仅影响开通本身,APP 重试即可。超时用 RequestConfig.setConnectTimeout/SocketTimeout(按配置 im.timeout-ms,默认 5000ms)控制。
Decision: 接口从 request.getHeader("token") 取 token,经 new JwtUtil().getusid(token) 解析 userId;为空则拒绝(返回未登录错误)。
Rationale: 与 NewebPayPayController(行 121-129)完全一致的鉴权模式,复用现有 JwtUtil,不引入新鉴权方式。不接受 APP 传 userId 参数,防越权为他人开通。
Decision: 新建 ImAccountVo,含 apiKey(String)、imUserId(String) 两字段,作为开通接口的 data 返回。
Rationale: APP 需要这两个值初始化 IM SDK。imUserId 用 String 规避前端精度问题(见决策 3)。
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 重写:技术债与重写风险高,弃。ImAccountService 保持分层。对外契约不变: POST /infouser/user/im/open 仍在 InfoUserController,请求/响应/行为与 contracts/open-im-account.md 完全一致,仅内部模块归属调整。
所有实现层面决策已明确,无遗留 NEEDS CLARIFICATION。可进入 Phase 1(data-model / contracts / quickstart)。