---

description: "Task list for 骑手与用户 IM 即时沟通账号接入"
---

# Tasks: 骑手与用户 IM 即时沟通账号接入

**Input**: Design documents from `/specs/012-im-user-integration/`

**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/ ✅, quickstart.md ✅

**Tests**: 项目无自动化测试约定，不生成测试任务；验证通过 quickstart.md 手动场景完成。

**Organization**: 按用户故事分组。注意：本特性 3 个故事围绕同一接口，存在依赖（US3 配置→US1 接口→US2 幂等），按依赖顺序排列。

## Format: `[ID] [P?] [Story] Description`

- **[P]**: 可并行（不同文件、无未完成依赖）
- **[Story]**: 所属用户故事（US1/US2/US3）
- 描述含确切文件路径

---

## Phase 1: Setup（数据库变更）

**Purpose**: info_user 表新增 IM 凭证两列

- [x] T001 在 `updatesql/sql.md` 追加 info_user 加列 SQL：`ALTER TABLE info_user ADD COLUMN im_api_key VARCHAR(64) DEFAULT NULL COMMENT 'IM平台API密钥';` 与 `ALTER TABLE info_user ADD COLUMN im_user_id BIGINT DEFAULT NULL COMMENT 'IM平台用户ID';`（标注日期 2026-06-23 与用途，不直接执行）

---

## Phase 2: Foundational（阻塞前置，所有故事依赖）

**Purpose**: 实体字段、映射、配置、外部客户端、返回 VO —— 用户故事开始前必须就绪

**⚠️ CRITICAL**: 本相位完成前不得开始用户故事

- [x] T002 [P] `ruoyi-system/src/main/java/com/ruoyi/system/domain/InfoUser.java`：新增 `imApiKey`(String) 与 `imUserId`(Long) 两字段（带 `@Excel` 注解与注释，IM apiKey / IM userId）；该类用 Lombok `@Data` 自动生成 getter/setter
- [x] T003 [P] `ruoyi-system/src/main/resources/mapper/infouser/InfoUserMapper.xml`：在 `InfoUserResult` resultMap 追加 `<result property="imApiKey" column="im_api_key"/>` 与 `<result property="imUserId" column="im_user_id"/>`（`selectInfoUserVo` 为 select *，无需改 SQL）
- [x] T004 [P] `ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/ImAccountVo.java`：新建返回 VO，含 `apiKey`(String) 与 `imUserId`(**String**，规避前端 Long 精度丢失)；用 `@Data`
- [x] T005 [P] `ruoyi-admin/src/main/resources/application.yml`：新增 `im` 配置段（base-url / ext-token / create-path / timeout-ms），测试值 base-url=`https://test-im.abtim-my.com`、ext-token=`92a88467-6eca-11f1-9dd5-00163e1eec55`、create-path=`/bot/extCreate`、timeout-ms=`5000`（参考现有 ezpay/newebpay 段风格）【满足 US3 配置集中管理】
- [x] T006 [P] `ruoyi-admin/src/main/java/com/ruoyi/app/utils/im/ImClient.java`：新建 `@Component`，`@Value` 注入 `im.base-url`/`im.ext-token`/`im.create-path`/`im.timeout-ms`；方法 `ImAccountVo createAccount()` 用 `org.apache.http`（仿 `NewebPay.postFormRaw`）发 `POST {base-url}{create-path}`，请求头带 `extToken`，设连接/读取超时，解析 fastjson2 响应：`code==200 && data.apiKey/userId 非空` → 填充 VO（imUserId 转 String）；否则抛 `ServiceException`【满足 US3 配置注入】

**Checkpoint**: 数据层 + 外部 IM 客户端就绪，可开始用户故事

---

## Phase 3: User Story 1 - APP 触发开通 IM 账号 (Priority: P1) 🎯 MVP

**Goal**: APP 调用 `POST /infouser/user/im/open`，为当前登录用户首次开通 IM 账号并返回凭证

**Independent Test**: 以未开通用户 token 调用接口 → 返回 apiKey/imUserId 且库中两列成对写入（见 quickstart 场景1）

### Implementation for User Story 1

- [x] T007 [US1] `ruoyi-system/src/main/java/com/ruoyi/system/service/IInfoUserService.java`：新增方法签名 `ImAccountVo openImAccount(Long userId);`
- [x] T008 [US1] `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/InfoUserServiceImpl.java`：注入 `ImClient`；实现 `openImAccount` **首次开通**逻辑：`getById(userId)` → （本期暂不做幂等分支，留 US2）→ `imClient.createAccount()` 取 VO → `updateById` 把 apiKey/imUserId 写回用户（成对）→ 返回 VO（注意 imUserId 落库为 Long，VO 用 String）。失败时 ImClient 已抛异常，本层不写库
- [x] T009 [US1] `ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java`：新增 `@PostMapping("/im/open")`（复用现有 `JwtUtil`/`request`，仿 NewebPayPayController 行121-129）：从 `request.getHeader("token")` 经 `new JwtUtil().getusid(token)` 解析 userId；为空返回未登录错误；否则调 `infoUserService.openImAccount(userId)`，用 `AjaxResult.success(data)` 返回 VO

**Checkpoint**: User Story 1 完整可用 —— 首次开通端到端跑通（幂等性暂缺，由 US2 补齐）

---

## Phase 4: User Story 2 - 重复开通幂等返回 (Priority: P2)

**Goal**: 同一用户重复调用开通接口时，直接返回已有凭证，不重复调用 IM 平台

**Independent Test**: 同一用户连调两次 → 返回凭证一致，IM 侧仅创建一次（见 quickstart 场景2）

### Implementation for User Story 2

- [x] T010 [US2] `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/InfoUserServiceImpl.java`：在 `openImAccount` 开头增加幂等分支——`getById(userId)` 后若 `imApiKey` 非空（已有凭证），直接组装并返回现有 VO（`imUserId` 转 String），**不再调用** `imClient.createAccount()`；否则走 T008 首次开通逻辑

**Checkpoint**: 幂等生效，重复调用安全

---

## Phase 5: User Story 3 - IM 配置集中管理 (Priority: P3)

**Goal**: IM 域名与 extToken 全部来自配置文件，代码无硬编码，环境切换零改码

**Independent Test**: 改 yml 域名/令牌 → 重启 → 调用指向新地址（见 quickstart 场景4 间接验证）

> 实现：US3 由 Foundational T005（yml 配置段）+ T006（@Value 注入）已交付。本相位为合规验证。

- [x] T011 [US3] 验证：grep 确认 `ImClient.java` 与全工程无硬编码 IM 域名（`test-im.abtim-my.com`）或 extToken 明文，均经 `@Value("${im.*}")` 读取；确认 yml 测试/正式环境可仅改配置切换

**Checkpoint**: 配置集中管理达标

---

## Phase 6: Polish & Cross-Cutting Concerns

**Purpose**: 跨故事质量与端到端验证

- [x] T012 [P] 精度复核：确认 `ImAccountVo.imUserId` 为 String，且 controller 返回 JSON 中 imUserId 为字符串（19 位不丢精度）；确认 `InfoUser.imUserId` 落库为 Long/BIGINT 不溢出
- [ ] T013 [P] 按 `specs/012-im-user-integration/quickstart.md` 执行场景 1~5 全部验证并记录结果
- [x] T014 [P] 回归确认：用户注册相关接口（骑手/商家/普通用户注册）行为与返回结构未被改动

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Phase 1)**: 无依赖，立即开始（T001 仅写 SQL 文件，不执行）
- **Foundational (Phase 2)**: T002~T006 可并行（不同文件）；本相位阻塞所有用户故事
- **US1 (Phase 3)**: 依赖 Foundational；内部 T007→T008→T009（接口→实现→控制器）
- **US2 (Phase 4)**: 依赖 US1 的 T008（在已有 service 方法上加幂等分支）
- **US3 (Phase 5)**: 实现已在 Foundational T005/T006；T011 仅验证
- **Polish (Phase 6)**: 依赖全部用户故事完成

### Within Each User Story

- 接口签名 → 实现 → 控制器（US1）
- 幂等分支在首次开通逻辑就绪后追加（US2 依赖 US1）

### Parallel Opportunities

- Foundational T002 / T003 / T004 / T005 / T006 可全部并行（不同文件）
- Polish T012 / T013 / T014 可并行

---

## Implementation Strategy

### MVP First（仅 User Story 1）

1. Phase 1：写 SQL（T001）
2. Phase 2：字段+映射+VO+配置+ImClient（T002~T006）
3. Phase 3：service+controller 首次开通（T007~T009）
4. **STOP 验证**：quickstart 场景1（首次开通）+ 场景5（全类型）
5. 可联调交付

### Incremental Delivery

1. Foundational → 基础就绪
2. +US1 → 首次开通可用（MVP）
3. +US2 → 重复调用幂等（健壮性）
4. +US3 验证 → 配置合规
5. Polish → 精度/回归/全场景

---

## Notes

- 数据库变更只写 `updatesql/sql.md`，不直接执行（项目规范）
- `imUserId` 在 VO 中用 String，落库用 Long/BIGINT（防前端 JS 精度丢失 + 容纳 19 位）
- 复用现有 `JwtUtil.getusid(token)` 解析用户，不新增鉴权方式
- 不改动注册流程（用户明确要求）
