# 「补全出行信息」弹窗三处缺陷（回显不全 / 紧急联系人必填 / 保存后不关闭） — 前端待修 + 后端已补强 — 管理后台

> 变更类型：🐛 前端缺陷（①回显、③保存后不关闭，无后端代码变更）+ 🔧 后端门禁补强（②紧急联系人必填，PR #4041，零 DDL、无新增接口）；本文档给出前端对接修复方式 + 数据丢失避坑
> 端类型：管理后台（订单详情页 → 「补全出行信息」弹窗）
> 日期：2026-06-19
> 服务：hl-order-service-v3（出行人补全接口已补紧急联系人必填门禁 581109 / 订单详情明文回显接口已上线）
> 责任端：前端 hl-ui（mmg，① ③ + ② 配合保持必填）；后端 order-v3（② 已修，PR #4041）
> 实测样本：测试服网关 9443 + 真实 admin token，订单 `HL20260618165816942`（orderId=`2067532338723504130`，定制中 / 待补全信息）

---

## ⚠️ 关键说明

问题 ① 回显不全、③ 保存后弹窗不关闭为**前端侧**；问题 ② 紧急联系人必填的**后端门禁已补强并部署测试服**（PR #4041），前端需配合「保持必填 + 处理新错误码」。后端经测试服逐项实测：

- 后端**有**该出行人的完整明文数据，且已提供**明文回显接口**（订单详情 overview，#3509）；
- 保存接口对「资料齐全」场景返回 **HTTP 200 `success=true`**（不是保存失败 → ③ 是前端没关弹窗）；
- 紧急联系人按**合同要求必填**：后端原先未校验（空值可落库），现已补强制校验（错误码 `581109`，PR #4041 已合并 dev-v3 + 部署测试服实测）。

> 🔴 **最高优先级警告（修复问题一时必读 §4）**：补全弹窗里被清空显示的「证件号 / 手机号」，若在保存时以**空字符串 `""`** 回传，后端会把数据库里**已有的真实证件号 / 手机号抹掉**（merge 语义：`null` 保留原值、`""` 覆盖为空）。修回显的同时必须改对保存回传，否则会造成线上数据丢失。

---

## 1. 问题一：回显信息不全（证件号 / 手机号 / 备注 显示为空，状态显示「待填」实为「已完成」）

**现象**：打开弹窗，出行人「王完善」的证件号码、手机号、客户备注均为空，出行人标签显示「待填」。

**实测对比（同一订单，后端实际数据 vs 弹窗显示）**：

| 字段 | 后端实际值（订单详情接口明文返回） | 弹窗显示 |
|---|---|---|
| 证件号 | `152522198605311079`（明文） | **空** |
| 手机号 | `18547062756`（明文） | **空** |
| 客户备注 | `测试测试 哈哈哈哈` | **空** |
| profileStatus | `COMPLETED`（已完成） | **待填** |
| 紧急联系人 | name=`1` / phone=`18547056215` | 正常显示 ✓ |

**根因**：弹窗的回显数据**没取自明文源**。后端有两个出行人查询口径：

1. `GET /v3/admin/order/{id}/traveler/list` —— **脱敏版**（#2894 PII 应急修复），证件号 / 手机号返回的是 `idCardMasked` / `phoneMasked`（形如 `152***********1079`、`185****2756`）。这种带星号的脱敏值**无法放进可编辑输入框**，前端只能留空 → 表现为「证件号 / 手机号 空」。
2. `GET /v3/admin/order/{id}`（订单详情 overview）→ `overview.customerInfo.travelers[]` —— **明文版**（`TravelerPlainVO`，#3509，admin 端业务例外），证件号字段名 `idCard`、手机号 `phone` 均为明文。

补全弹窗是从**订单详情页**打开的，订单详情数据前端**已经加载过**（含明文出行人 + 紧急联系人 + 客户备注）。弹窗应直接用这份已加载的明文数据回填，而不是再调脱敏的 `/traveler/list`。

**修复方式**：弹窗回显改用订单详情明文源：

| 弹窗字段 | 取自订单详情（`GET /v3/admin/order/{id}` → `data.overview`） |
|---|---|
| 出行人 姓名/性别/生日/证件类型 | `customerInfo.travelers[].name / gender / birthday / idType` |
| 出行人 **证件号** | `customerInfo.travelers[].idCard`（明文） |
| 出行人 **手机号** | `customerInfo.travelers[].phone`（明文） |
| 出行人 状态标签 | `customerInfo.travelers[].profileStatus`（直接读，`COMPLETED`=已完成，别再用「输入框是否为空」反推，否则永远显示待填） |
| 紧急联系人 姓名/电话 | `customerInfo.emergencyContactName / emergencyContactPhone` |
| 客户备注 | `remarkInfo.customerRemark` |

```bash
# 回显数据源：订单详情 overview（明文）
curl -k "https://api.test.1814.love:9443/v3/admin/order/2067532338723504130" \
  -H "Authorization: Bearer <adminToken>"
```

实测返回（节选，已上线）：

```json
{
  "code": 200,
  "data": {
    "overview": {
      "customerInfo": {
        "emergencyContactName": "1",
        "emergencyContactPhone": "18547056215",
        "travelers": [
          {
            "id": "2067803630752174082",
            "name": "王完善", "gender": "1", "birthday": "1986-05-31",
            "idType": "ID_CARD", "idCard": "152522198605311079", "phone": "18547062756",
            "profileStatus": "COMPLETED", "nationality": "中国", "race": "汉族"
          }
        ]
      },
      "remarkInfo": { "customerRemark": "测试测试 哈哈哈哈" }
    }
  }
}
```

---

## 2. 问题二：紧急联系人必填（合同要求）— 后端已补强制校验

**结论**：紧急联系人**按合同要求必须填写**（弹窗副标题「根据合同要求，需填写所有出行人证件号与紧急联系人信息」即此意）。前端保持必填即可。

**后端补强（PR #4041，已合并 dev-v3 + 部署测试服）**：此前后端 `saveTravelerInfo` 对紧急联系人**无任何校验**，空值提交会成功落库（合同必报字段只靠前端单点拦截不安全）。现已在后端补强制门禁，与前端必填形成双层保障：

| 场景 | 后端返回 |
|---|---|
| 紧急联系人姓名 / 电话任一为空 | `581109`「紧急联系人姓名和电话必填」，不落库 |
| 紧急联系人电话格式非法（非 11 位） | `581113`「手机号格式非法（应为 11 位数字）」 |
| 姓名 + 合法电话齐全 | 200 正常保存 |

**前端配合**：
1. **保持**紧急联系人姓名 + 电话的「必填」前端校验（不要去掉），弹窗副标题维持「需填写紧急联系人」。
2. 透传后端 `581109` / `581113` 的 `message` 给用户（用户绕过前端校验或格式不对时的兜底提示）。

---

## 3. 问题三：保存成功后弹窗不关闭

**现象**：点「保存并完成」后弹窗一直不消失。

**实测**：保存接口 `PUT /v3/admin/order/{id}/traveler-info` 对该场景返回 **HTTP 200**：

```json
{ "code": 200, "success": true,
  "data": { "createdCount": 0, "updatedCount": 1, "deletedCount": 0,
            "completedCount": 1, "pendingCount": 0, "allCompleted": true } }
```

保存是**成功的**，后端无报错。弹窗不关闭是前端**没有在成功响应后关闭弹窗 / 刷新页面**。

**修复方式**：

1. 保存成功（`code===200 && success===true`）后：关闭弹窗 + 刷新订单详情（出行人区、流程状态）。
2. ⚠️ 判成功要看**业务字段** `code===200 && success===true`，**不能只看 HTTP 状态码**。原因：保存接口带幂等保护 `@Idempotent`，**同订单 3 秒内重复提交**会被拦截，此时 **HTTP 仍是 200 但 `success=false`、`code!=200`**（实测复现：连续两次提交，第二次 `success=false`）。前端若只看 HTTP 200 就当成功，会把「被幂等拦截、其实没保存」误判为成功；若据此关弹窗也属误关。正确做法：仅 `success===true` 关弹窗，`success===false` 弹出后端 `message` 提示用户。

---

## 4. ⚠️ 数据丢失陷阱（修问题一时必须同步处理）

保存接口的 merge 语义（`mergeFromEditItem`）：**某字段传 `null`（或不传该 key）= 保留数据库原值；传任何非 null 值（含空字符串 `""`）= 覆盖**。

结合问题一的回显修复，存在数据丢失风险链：

- 出行人「王完善」DB 里证件号 = `152522198605311079`、手机号 = `18547062756`（已完成）；
- 若弹窗回显仍把证件号 / 手机号显示为空，用户没动这两个框，保存时前端把 `idNo:""`、`phone:""` 回传；
- 后端 `"" != null` → **把真实证件号 / 手机号覆盖成空** → 该出行人从 `COMPLETED` 退回 `PENDING`，**证件号 / 手机号永久丢失**。

**前端务必遵守**：

1. **优先**：按 §1 用明文源回填证件号 / 手机号，用户未修改时**原样回传明文值**（保存接口字段名是 `idNo` / `phone`，注意回显源字段名是 `idCard` / `phone`，证件号需做 `idCard → idNo` 映射）。
2. **退路**：某字段确实没有可回填的值、或不想回传，就传 `null` / 不带该 key（保留原值），**绝不能传空字符串 `""`**。
3. 仅当用户**主动清空**某字段且业务确实要清空时，才传 `""`。

---

## 5. 接口契约速查

**回显**：`GET /v3/admin/order/{id}` → `data.overview.customerInfo`（明文出行人 + 紧急联系人）+ `data.overview.remarkInfo.customerRemark`。

**保存**：`PUT /v3/admin/order/{id}/traveler-info`

| 字段 | 必填 | 说明 |
|---|---|---|
| travelers[] | 是（≤30，非空） | 全量同步：`id=null` 新增 / `id` 非空 更新 / DB 有而本次没提交的 → 删除 |
| travelers[].id | 更新必填 | 回显时务必带上，否则会被当新增 + 旧行被删 |
| travelers[].birthday | **是**（`@NotNull`） | 出生日期，后端按年龄派生出行人类型 |
| travelers[].name / gender / idType | 否 | 缺则该出行人停留 PENDING |
| travelers[].**idNo** | 否 | 证件号**明文**回传（回显源字段叫 `idCard`，注意映射）；空串会覆盖，见 §4 |
| travelers[].**phone** | 否 | 手机号明文回传；空串会覆盖，见 §4 |
| travelers[].nationality / race | 否 | 不传后端默认「中国」/「汉族」；**传空串 `""` 会被拒**（`581101`） |
| emergencyContactName / emergencyContactPhone | **是**（合同要求，见 §2） | 订单级紧急联系人；姓名 + 电话必填（任一空 → `581109`），电话须合法 11 位（→ `581113`） |
| customerRemark | 否 | 客户备注 |

**幂等**：同订单 3 秒窗口重复提交被拒（HTTP 200 但 `success=false`），前端按 §3 判 `success`。

---

## 6. 实测确认（测试服 9443 + 真实 admin token，订单 2067532338723504130）

- `GET /v3/admin/order/{id}`：明文证件号 `152522198605311079`、手机号 `18547062756`、备注 `测试测试 哈哈哈哈`、profileStatus `COMPLETED` 均正常返回 ✓
- `GET /v3/admin/order/{id}/traveler/list`：证件号为脱敏 `152***********1079`、手机号 `185****2756`（确认前端用错了脱敏源会留空）✓
- `PUT /v3/admin/order/{id}/traveler-info`（保留明文值）：HTTP 200 `success=true`、`allCompleted=true` ✓
- `PUT`（空紧急联系人，PR #4041 部署后）：返 `581109`「紧急联系人姓名和电话必填」、不落库 ✓
- `PUT`（紧急联系人电话格式非法）：返 `581113`「手机号格式非法」 ✓
- `PUT`（姓名 + 合法电话齐全）：HTTP 200 `success=true` 回归正常 ✓
- 连续两次 `PUT`：第二次 `success=false`（确认 3 秒幂等窗口，前端需判 `success`）✓
- 测试结束已将订单数据恢复原状（紧急联系人 `1`/`18547056215`、证件号、profileStatus 均完好）✓

---

## 备注

- 问题 ①③ 为前端缺陷（**无后端代码变更**），走本 changelog 通知前端同事，不建工单。
- 问题 ② 紧急联系人必填后端门禁经 **PR #4041 补强**（新增错误码 `581109`，复用 `581113`，**无 DDL、无新增接口**），已合并 dev-v3 + 部署测试服实测；对应工单 **#4040**（已关）。
- 后端明文回显能力（`TravelerPlainVO`）为 #3509 已上线特性，admin 端业务例外、需 admin 权限，前端可放心使用。
