11 KiB
「补全出行信息」弹窗三处缺陷(回显不全 / 紧急联系人误必填 / 保存后不关闭) — 前端待修 — 管理后台
变更类型:🐛 前端缺陷(无后端代码变更、零 DDL;后端接口与数据经测试服实测均正常,本文档给出对接修复方式 + 数据丢失避坑) 端类型:管理后台(订单详情页 → 「补全出行信息」弹窗) 日期:2026-06-19 服务:hl-order-service-v3(出行人 / 订单详情接口,均已上线且行为正常) 责任端:前端 hl-ui(mmg) 实测样本:测试服网关 9443 + 真实 admin token,订单
HL20260618165816942(orderId=2067532338723504130,定制中 / 待补全信息)
⚠️ 关键说明
这三个问题全部在前端侧。后端经测试服逐项实测均正常:
- 后端有该出行人的完整明文数据,且已提供明文回显接口(订单详情 overview,#3509);
- 保存接口对该场景返回 HTTP 200
success=true(不是保存失败); - 紧急联系人后端不要求必填(VO 无
@NotBlank,Service 无校验),空值提交返回 200。
🔴 最高优先级警告(修复问题一时必读 §4):补全弹窗里被清空显示的「证件号 / 手机号」,若在保存时以空字符串
""回传,后端会把数据库里已有的真实证件号 / 手机号抹掉(merge 语义:null保留原值、""覆盖为空)。修回显的同时必须改对保存回传,否则会造成线上数据丢失。
1. 问题一:回显信息不全(证件号 / 手机号 / 备注 显示为空,状态显示「待填」实为「已完成」)
现象:打开弹窗,出行人「王完善」的证件号码、手机号、客户备注均为空,出行人标签显示「待填」。
实测对比(同一订单,后端实际数据 vs 弹窗显示):
| 字段 | 后端实际值(订单详情接口明文返回) | 弹窗显示 |
|---|---|---|
| 证件号 | 152522198605311079(明文) |
空 |
| 手机号 | 18547062756(明文) |
空 |
| 客户备注 | 测试测试 哈哈哈哈 |
空 |
| profileStatus | COMPLETED(已完成) |
待填 |
| 紧急联系人 | name=1 / phone=18547056215 |
正常显示 ✓ |
根因:弹窗的回显数据没取自明文源。后端有两个出行人查询口径:
GET /v3/admin/order/{id}/traveler/list—— 脱敏版(#2894 PII 应急修复),证件号 / 手机号返回的是idCardMasked/phoneMasked(形如152***********1079、185****2756)。这种带星号的脱敏值无法放进可编辑输入框,前端只能留空 → 表现为「证件号 / 手机号 空」。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 |
# 回显数据源:订单详情 overview(明文)
curl -k "https://api.test.1814.love:9443/v3/admin/order/2067532338723504130" \
-H "Authorization: Bearer <adminToken>"
实测返回(节选,已上线):
{
"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. 问题二:紧急联系人被前端设为必填(后端并不要求)
现象:不填紧急联系人无法保存(前端拦截)。
实测:保存接口传空紧急联系人(emergencyContactName=""、emergencyContactPhone="")→ HTTP 200,success=true,保存成功。后端 TravelerInfoSaveReqVO 的 emergencyContactName / emergencyContactPhone 无 @NotBlank,Service 层也无必填校验。
修复方式:前端去掉紧急联系人两个字段的「必填」校验。弹窗副标题「需填写……紧急联系人信息」也应同步改为非强制措辞(如「如有请填写紧急联系人」)。
注:紧急联系人手机号若填了,后端会校验 11 位手机号格式(不合法返
581113);不填则跳过。即「非必填,但填了要合法」。
3. 问题三:保存成功后弹窗不关闭
现象:点「保存并完成」后弹窗一直不消失。
实测:保存接口 PUT /v3/admin/order/{id}/traveler-info 对该场景返回 HTTP 200:
{ "code": 200, "success": true,
"data": { "createdCount": 0, "updatedCount": 1, "deletedCount": 0,
"completedCount": 1, "pendingCount": 0, "allCompleted": true } }
保存是成功的,后端无报错。弹窗不关闭是前端没有在成功响应后关闭弹窗 / 刷新页面。
修复方式:
- 保存成功(
code===200 && success===true)后:关闭弹窗 + 刷新订单详情(出行人区、流程状态)。 - ⚠️ 判成功要看业务字段
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 用明文源回填证件号 / 手机号,用户未修改时原样回传明文值(保存接口字段名是
idNo/phone,注意回显源字段名是idCard/phone,证件号需做idCard → idNo映射)。 - 退路:某字段确实没有可回填的值、或不想回传,就传
null/ 不带该 key(保留原值),绝不能传空字符串""。 - 仅当用户主动清空某字段且业务确实要清空时,才传
""。
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) | 订单级紧急联系人;电话填了要合法 11 位 |
| customerRemark | 否 | 客户备注 |
幂等:同订单 3 秒窗口重复提交被拒(HTTP 200 但 success=false),前端按 §3 判 success。
6. 实测确认(测试服 9443 + 真实 admin token,订单 2067532338723504130)
GET /v3/admin/order/{id}:明文证件号152522198605311079、手机号18547062756、备注测试测试 哈哈哈哈、profileStatusCOMPLETED均正常返回 ✓GET /v3/admin/order/{id}/traveler/list:证件号为脱敏152***********1079、手机号185****2756(确认前端用错了脱敏源会留空)✓PUT /v3/admin/order/{id}/traveler-info(保留明文值):HTTP 200success=true、allCompleted=true✓PUT(空紧急联系人):HTTP 200success=true(确认紧急联系人非必填)✓- 连续两次
PUT:第二次success=false(确认 3 秒幂等窗口,前端需判success)✓ - 测试结束已将订单数据恢复原状(紧急联系人
1/18547056215、证件号、profileStatus 均还原)✓
备注
- 本文为前端缺陷通知 + 现有接口对接说明,无后端代码变更、零 DDL、无新增 / 改动接口。
- 不建 Gitea 工单(前端 bug 走 changelog 通知前端同事)。
- 后端明文回显能力(
TravelerPlainVO)为 #3509 已上线特性,admin 端业务例外、需 admin 权限,前端可放心使用。