# P9→NS1/RNS1 缺失项 #19：客户详情页电话号码的脱敏展示

## 简要结论
- 状态：✅ 已解决
- 风险等级：🟢 低
- 后端已实现 `_mask_phone()` 脱敏函数（中间 4 位用 `****`），前端实现了"点击查看/复制"交互

## 详细审查

### 审查范围
- `apps/backend/app/services/customer_service.py` — `_mask_phone()` 函数、`get_customer_detail()` 返回
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.wxml` — 电话展示区域
- `apps/miniprogram/miniprogram/pages/customer-detail/customer-detail.ts` — 电话交互逻辑
- `apps/miniprogram/miniprogram/pages/customer-service-records/customer-service-records.ts` — 同样的脱敏逻辑

### 发现
1. 后端 `_mask_phone()` 实现：`phone[:3] + "****" + phone[-4:]`，如 `139****5678`
2. 后端 `get_customer_detail()` 同时返回 `phone`（脱敏）和 `phone_full`（完整），供前端按需展示
3. customer-detail 前端实现了 `phoneVisible` 状态切换：
   - 默认显示脱敏号码 `138****5678`
   - 点击"查看"按钮切换为完整号码
   - 显示完整号码后按钮变为"复制"，调用 `wx.setClipboardData`
4. customer-service-records 页面也实现了相同的脱敏+查看交互：
   - 前端自行脱敏：`detail.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')`
   - 同样有 `onTogglePhone()` 和 `onCopyPhone()` 方法

### 证据
```python
# customer_service.py
def _mask_phone(phone: str | None) -> str:
    """手机号脱敏：139****5678 格式。"""
    if not phone or len(phone) < 7:
        return phone or ""
    return f"{phone[:3]}****{phone[-4:]}"
```

```html
<!-- customer-detail.wxml -->
<text class="phone">{{phoneVisible ? detail.phone : '138****5678'}}</text>
<view class="phone-toggle-btn" bindtap="{{phoneVisible ? 'onCopyPhone' : 'onTogglePhone'}}">
  <text class="phone-toggle-text">{{phoneVisible ? '复制' : '查看'}}</text>
</view>
```

### 建议
- customer-detail.wxml 中脱敏号码硬编码为 `138****5678`，应改为使用后端返回的 `phone` 字段（已脱敏）
- 建议统一脱敏策略：要么全部由后端脱敏，要么全部由前端脱敏，避免两端重复实现
