# refactor: 定制师反查链路改 by-userid / by-openid (手机号 PII 不出 user-service + 性能减半)

> **仓库**: HL (后端 hl-user-service + hl-mp-service + hl-order-service-v2 + hl-common-feign + hl-common-log)
> **关联 PR/Issue**: PR #2924,**无关联工单**(二轮深度 CR 整改)
> **日期**: 2026-05-23
> **影响范围**: 内部反查接口签名变化,前端**无感知**
> **接收方**: mmg (前端)
> **前端**: **无需变更** — 本 PR 全是后端内部 Feign 链路重构,`/mp/customizer/me` 和下单 `sharerOpenid` 反查的对外行为完全一致

---

## 🎯 核心改动 (二轮 CR P0 整改)

PR #2913 (定制师分享反查) 引入的反查链路把客人/定制师真实手机号通过 Feign URL query 在内网服务间明文流转:

- mp 端 `/mp/customizer/me`: `getProfile(userId)` 取 `rawPhone` → `getCustomizerByPhone(phone)` 两次串行
- order-v2 端分享下单: `getUserByOpenid(openid)` 取 `phone` → `getCustomizerByPhone(phone)` 两次串行

**问题**:
- 手机号会出现在 nginx access log + SkyWalking 分布式 trace 中,**绕过 `maskPhone` 脱敏**
- 串行 2 次 Feign,P99 延迟翻倍

**重构方案**: user-service 内部一次性完成 `userId/openid → user.phone → admin_user.mobile → CUSTOMIZER 校验`,**phone 永不出 user-service 进程**

---

## 一、新增内部接口 (Feign,不经网关)

### `GET /internal/user/customizer/by-userid?userId=xxx` (mp 端用)

| 字段 | 类型 | 说明 |
|------|------|------|
| code | int | 200 |
| data | `CustomizerByPhoneInternalVO` 或 null | 命中的 CUSTOMIZER admin 信息 |

匹配规则: `user.user_id → user.phone → admin_user.mobile == phone + status=ACTIVE + 拥有 CUSTOMIZER 角色`

### `GET /internal/user/customizer/by-openid?openid=xxx` (order-v2 端用)

参数/返回同上,只是入口换成 `openid`(`user.openid → user.phone → admin_user.mobile`).

### 旧 endpoint `/internal/user/admin/by-phone?phone=xxx` 保留

兼容其他可能的调用方,暂不下线 (后续会陆续迁移)。

---

## 二、调用方变化 (对前端零影响)

### `GET /mp/customizer/me`

| 项 | 之前 | 现在 |
|----|------|------|
| Feign 调用次数 | 2 次串行 (`getProfile` + `getCustomizerByPhone`) | **1 次** (`getCustomizerByUserId`) |
| 真实手机号传输 | 走第二次 Feign URL query | **不出 user-service** |
| 响应字段 | 不变 | 不变 |
| 行为 | 不变 | 不变 |

### `POST /mp/order/create` 的 `sharerOpenid` 反查

| 项 | 之前 | 现在 |
|----|------|------|
| Feign 调用次数 | 2 次串行 (`getUserByOpenid` + `getCustomizerByPhone`) | **1 次** (`getCustomizerByOpenid`) |
| 真实手机号传输 | 走第二次 Feign URL query | **不出 user-service** |
| 行为 | 不变 (兜底 / 命中 / 反查异常 三个分支保留) | 不变 |

---

## 🚨 前端需要做的事

**什么都不需要做**。

- `/mp/customizer/me` 响应字段无变化,前端按既有逻辑渲染即可
- `POST /mp/order/create` 接口无变化
- 错码无新增

可选自查:
```bash
grep -rE "/mp/customizer/me|sharerOpenid" hl-ui/src mp-cli/src
```

---

## 三、附带优化 (二轮 CR P1)

### P1#11 — SMS 登录路径重复代码抽 helper

`AdminAuthService.loginBySms` 中 3 段几乎一模一样的「incr fail count + judge locked/remaining + throw」(码错 / mobile 不绑 / 非定制师 3 个分支),抽 private `buildSmsFailOrLocked(failKey, currentFail)`。**行为完全不变**,既有 16 个 SMS 单测全部通过。

### P1#13 — GlobalExceptionHandler.handleLoginFail 补单测

PR #2913 引入的 `handleLoginFail` 拦截器原本 0 覆盖率,补 3 个 case:
- `remainingAttempts → LoginFailDetailVO{remainingAttempts=3, lockedMinutes=null}`
- `locked → LoginFailDetailVO{remainingAttempts=null, lockedMinutes=15}`
- `zeroRemaining` 边界 → `remainingAttempts=0` 如实回传

---

## 四、测试服验证记录

部署 user + mp + order-v2 三服务 (rolling-deploy-all,2026-05-23 00:11) 后真测:

| # | 场景 | 实测 |
|---|------|------|
| T1 | CUSTOMIZER user (wx, userId=...4314) → SMS 登录 → `/mp/customizer/me` | ✅ `isCustomizer=true, adminId=...8914, customizerName=王骁` (新 `getCustomizerByUserId` 走通) |
| T2 | 未登录直接调 `/mp/customizer/me` | ✅ 200 + `isCustomizer=false` (P1#13 拦截器返 200 兜底) |
| T3 | nginx access log + user-service 自身日志 grep `phone=18*` | ✅ 0 命中 (手机号不再出现在日志) |
| 单测 | hl-user-service 129 + order-v2 OrderCreateServiceTest 28 + mp MpCustomizerAggregationServiceTest 4 + common-log GlobalExceptionHandlerTest 33 | ✅ 全绿 |

---

## 五、不在本期范围 (有意保留,二轮 CR 评估后)

- VO 命名 8 元组改名 (`AdminLoginResponse → AdminLoginRespVO` 等) — changelog 已发字段名不能动,改类名/移包风险大于收益
- `@Transactional` 边界缩窄 — 改动面大,既有测试容易破
- `getCustomizerByMobileForFeign` 加 Redis 缓存 — P2 性能优化,QPS 未观察到瓶颈
- `doSendSms` 异步化防 timing 攻击 — 实际可利用性低 (要先拿到正确的 SMS code)
- `MpUserFeignClient.getProfile` Map<String,Object> 改 UserVO — 历史包袱,mp 端已不再用 `getProfile` 取 phone (被新 by-userid 替代),问题自然消解

---

## 六、联系人

后端: wx (呼籁旅行)
前端: mmg

如有疑问可在 [PR #2924](https://git.1814.love:8443/wx/HL/pulls/2924) 评论区留言。
