diff --git a/backend/api/internal/logic/platform/gasorder/gasorder.go b/backend/api/internal/logic/platform/gasorder/gasorder.go index 5f427fd..500f9f3 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder.go @@ -175,7 +175,27 @@ func getGasorderBasic(ctx *gin.Context) { infra.Response.Error(ctx, err) return } - infra.Response.Success(ctx, common.ProtectPreciseLocation(ctx, &models.GasorderBasic{}, response)) + protected := common.ProtectPreciseLocation(ctx, &models.GasorderBasic{}, response) + infra.Response.Success(ctx, restorePlatformGasorderDetailContacts(protected, order)) +} + +// restorePlatformGasorderDetailContacts 仅为已通过平台订单菜单鉴权的详情接口恢复联系人快照明文。 +// 参数:value 为完成公共字段保护后的详情响应,order 为当前订单数据库记录。 +// 返回值:恢复联系人姓名和电话、且移除对应脱敏辅助键后的详情响应。 +func restorePlatformGasorderDetailContacts(value any, order models.GasorderBasic) any { + response, ok := value.(map[string]any) + if !ok { + return value + } + detail, ok := response["order"].(map[string]any) + if !ok { + return value + } + detail["contact_name"] = order.ContactName + detail["contact_phone"] = order.ContactPhone + delete(detail, "contact_name_masked") + delete(detail, "contact_phone_masked") + return value } func CreateGasorderContract(ctx *gin.Context) { diff --git a/backend/api/internal/logic/platform/gasorder/gasorder_test.go b/backend/api/internal/logic/platform/gasorder/gasorder_test.go index 2b103e6..a38380d 100644 --- a/backend/api/internal/logic/platform/gasorder/gasorder_test.go +++ b/backend/api/internal/logic/platform/gasorder/gasorder_test.go @@ -285,3 +285,25 @@ func TestGasorderSensitiveContactsAreMasked(t *testing.T) { } } } + +// TestPlatformGasorderDetailRestoresContacts 验证平台订单详情受控恢复联系人明文并删除脱敏辅助键。 +func TestPlatformGasorderDetailRestoresContacts(t *testing.T) { + value := map[string]any{ + "order": map[string]any{ + "contact_name_masked": "张*", + "contact_phone_masked": "138****8000", + }, + } + order := models.GasorderBasic{ContactName: "张三", ContactPhone: "13800138000"} + restored := restorePlatformGasorderDetailContacts(value, order).(map[string]any) + detail := restored["order"].(map[string]any) + if detail["contact_name"] != "张三" || detail["contact_phone"] != "13800138000" { + t.Fatalf("平台订单详情未恢复联系人明文: %#v", detail) + } + if _, exists := detail["contact_name_masked"]; exists { + t.Fatalf("平台订单详情仍包含联系人脱敏辅助键: %#v", detail) + } + if _, exists := detail["contact_phone_masked"]; exists { + t.Fatalf("平台订单详情仍包含联系电话脱敏辅助键: %#v", detail) + } +} diff --git a/docs/05-平台总后台需求.md b/docs/05-平台总后台需求.md index 4e0472c..ac5d62f 100644 --- a/docs/05-平台总后台需求.md +++ b/docs/05-平台总后台需求.md @@ -90,7 +90,7 @@ - 气站、配送点、人员和用户菜单可读取其详情内的钱包摘要;提现菜单可读取钱包信息用于审核。 - 角色的定位范围只允许 `standard`(脱敏坐标)或 `precise`(精确坐标)。root 默认使用精确坐标。 - 前端菜单隐藏只负责展示,最终权限由后端中间件执行。 -- 已通过平台总后台 JWT 和对应菜单权限校验的管理员,在用户、工作人员和平台账户的列表与详情中可查看姓名、显示名称和主手机号明文;该例外不扩展到其他终端,也不放宽身份证明、地址、头像、资质编号、订单联系人或收款账户的保护规则。 +- 已通过平台总后台 JWT 和对应菜单权限校验的管理员,在用户、工作人员和平台账户的列表与详情中可查看姓名、显示名称和主手机号明文。配送订单联系人另设最小范围例外:仅订单详情接口可返回订单联系人姓名和联系电话明文,订单列表及其他终端继续脱敏。上述例外不放宽身份证明、地址、头像、资质编号或收款账户的保护规则。 ## 5. 首页与报表 diff --git a/docs/11-数据接口与安全.md b/docs/11-数据接口与安全.md index cb99ab1..d8c513a 100644 --- a/docs/11-数据接口与安全.md +++ b/docs/11-数据接口与安全.md @@ -95,7 +95,7 @@ - 登录令牌短期有效,刷新令牌可撤销;后台高权限账号启用 MFA、IP/设备策略。平台后台管理的平台、气站、配送、员工和业主账号密码按当前实施口径仅要求不少于 6 个字符,不附加复杂度校验。 - 权限校验在服务端执行,前端菜单隐藏不构成权限控制。按角色、站点、区域、对象归属联合鉴权。 -- 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、访问留痕、最小化留存。默认响应继续脱敏;仅已通过平台总后台 JWT 和对应菜单权限校验的管理员,可在用户、工作人员和平台账户的列表与详情中查看姓名、显示名称和主手机号明文。用户地址资源另设最小范围例外:仅 `/user_address` 的受控列表与详情接口可向具备对应菜单权限的平台管理员返回完整地址及经纬度,用于地址维护和编辑回填。上述例外不适用于其他终端,也不放宽身份证明、头像、资质编号、订单联系人或收款账户的脱敏规则。 +- 手机号、地址、身份证明、收款账户、定位、视频为敏感数据:传输 TLS、存储加密/字段加密、访问留痕、最小化留存。默认响应继续脱敏;仅已通过平台总后台 JWT 和对应菜单权限校验的管理员,可在用户、工作人员和平台账户的列表与详情中查看姓名、显示名称和主手机号明文。用户地址资源另设最小范围例外:仅 `/user_address` 的受控列表与详情接口可向具备对应菜单权限的平台管理员返回完整地址及经纬度,用于地址维护和编辑回填。配送订单联系人另设详情级例外:仅 `/gasorder_basic/:identity` 平台详情接口可返回订单联系人姓名和联系电话明文,列表与其他终端继续脱敏。上述例外不放宽身份证明、头像、资质编号或收款账户的脱敏规则。 - 所有支付回调验证签名与金额、订单、商户号一致性;合同文件使用可信第三方原文与哈希存证。渠道回调入口为 `/heqi/payment-return/v1/{alipay|wechat}/notify`,不使用用户 JWT;必须完成渠道证书验签、商户/appid、平台支付单号、金额、币种和状态校验后才可在数据库事务中推进业务。重复通知必须幂等,原始敏感报文只保存摘要。 - `payment_order` 是统一支付尝试事实;`payment_refund` 与 `payment_refund_item` 保存用户退款申请及明细。审批通过与钱包入账必须同事务完成。 - 图片/视频上传做文件类型、大小、病毒/恶意内容检测;访问采用短期授权,不使用公开桶。 diff --git a/docs/12-验收与迭代规划.md b/docs/12-验收与迭代规划.md index 2aca843..598916a 100644 --- a/docs/12-验收与迭代规划.md +++ b/docs/12-验收与迭代规划.md @@ -46,7 +46,7 @@ - 可用性:安全告警、设备控制、支付回调等关键链路定义 SLO 和降级方案;上线前完成故障演练。 - 性能:以目标设备数量、每设备上报频率、峰值下单量、消息量为基准压测;容量指标须在立项时量化。 - 安全:完成权限越权、支付回调伪造、设备身份伪造、文件上传、敏感信息泄漏和常见 Web/App 攻击测试。 -- 隐私展示:平台总后台通过 JWT 和菜单鉴权后,用户、工作人员和平台账户列表与详情可显示姓名、显示名称和主手机号明文;其他终端、未授权请求及身份证明、地址、头像、资质编号、订单联系人和收款账户仍保持脱敏或不返回。 +- 隐私展示:平台总后台通过 JWT 和菜单鉴权后,用户、工作人员和平台账户列表与详情可显示姓名、显示名称和主手机号明文;配送订单联系人仅在平台订单详情显示明文,订单列表继续脱敏;其他终端、未授权请求及身份证明、地址、头像、资质编号和收款账户仍保持脱敏或不返回。 - 可观测性:每个关键请求可用 `request_id` 串联;每个设备命令、告警和订单状态可定位日志、指标和事件。 - 可用性与无障碍:移动端具备弱网提示、重要操作反馈、可读的风险颜色与文字;后台关键表格可键盘操作。 diff --git a/docs/操作日志_配送订单联系人受控展示_20260815.md b/docs/操作日志_配送订单联系人受控展示_20260815.md new file mode 100644 index 0000000..c29e85e --- /dev/null +++ b/docs/操作日志_配送订单联系人受控展示_20260815.md @@ -0,0 +1,34 @@ +# 配送订单联系人受控展示操作日志 + +操作时间:2026-08-15 +操作类型:扩展 +影响模块:平台总后台、配送订单详情、敏感字段保护 + +## 操作前状态 + +- 平台配送订单详情仅返回脱敏后的联系人姓名和联系电话。 +- 前端可能同时渲染业务字段及其 `*_masked` 辅助字段,造成联系电话重复展示。 + +## 具体操作 + +- 保留全局脱敏规则,仅在已通过平台 JWT 和配送订单菜单权限校验的订单详情接口恢复联系人快照明文。 +- 平台订单列表、其他终端及其他敏感字段继续执行原有脱敏规则。 +- 详情响应恢复明文后删除联系人姓名和电话的脱敏辅助键。 +- 前端通用详情渲染跳过已经由业务字段承载的 `*_masked` 辅助键。 +- 同步平台需求、安全规范和验收边界。 + +## 操作后状态 + +- 有权访问平台配送订单详情的管理员可直接查看完整联系人姓名和联系电话。 +- 联系电话只展示一次,不再出现重复字段。 +- 数据库结构、订单快照和其他客户端接口保持不变。 + +## 验证结果 + +- 后端专项测试覆盖详情明文恢复、脱敏辅助键删除和全局脱敏规则保持。 +- 前端专项检查覆盖脱敏辅助字段去重。 + +## 风险评估 + +- 明文仅在单笔订单详情中受控展示,避免列表批量暴露。 +- 继续沿用平台接口访问日志,不新增数据库审计结构。 diff --git a/frontend/platform_admin/scripts/check-gasorder-list-display.mjs b/frontend/platform_admin/scripts/check-gasorder-list-display.mjs index f54b6de..3af5323 100644 --- a/frontend/platform_admin/scripts/check-gasorder-list-display.mjs +++ b/frontend/platform_admin/scripts/check-gasorder-list-display.mjs @@ -12,6 +12,7 @@ const assertions = [ [resources.includes("payable_amount: '应付金额(元)'"), '订单应付金额缺少中文名称'], [detailPage.includes("'contract_display_name'"), '订单详情未隐藏重复的合同标题辅助字段'], [detailPage.includes("column.endsWith('_at')) return 150"), '关联记录的日期时间列未固定为 150px'], + [detailPage.includes("key.endsWith('_masked')"), '详情页未去除重复的脱敏辅助字段'], [resources.includes("listDisplayKey: 'creator_display_name'"), '创建方未配置可读名称字段'], [resources.includes('listDisplayIdentityCopy: true'), '创建方未保留唯一标识复制入口'], [resources.includes("detailDisplayKey: 'contract_display_name'"), '订单详情未配置合同标题展示字段'], diff --git a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue index 5345da0..30b8bf3 100644 --- a/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue +++ b/frontend/platform_admin/src/views/resource/ResourceDetailContent.vue @@ -108,6 +108,12 @@ const entries = computed(() => { ]; const keys = [...new Set([...preferred, ...Object.keys(row)])]; return keys.flatMap((key) => { + // 已由原始业务字段回退展示的脱敏辅助键不再重复渲染。 + if ( + key.endsWith('_masked') && + props.definition.fields.some((field) => field.key === key.slice(0, -7)) + ) + return []; const maskedKey = `${key}_masked`; const actualKey = Object.prototype.hasOwnProperty.call(row, key) ? key