Status: done

# 换货绑定迁移修复

## Parent

`.scratch/customer-binding-architecture/PRD.md`

## What to build

在 `CustomerBinding` 模块上实现 `Migrate(ctx, tx, oldAsset, newAsset)` 方法，替换换货服务中现有的 `switchCustomerBindingWithTx` 和 `ensureNewAssetBindingAvailableWithTx` 逻辑，修复有虚拟号旧卡换无虚拟号新卡时被误拦截的 bug。

### Migrate(ctx, tx, oldAsset, newAsset)

处理四种虚拟号组合，仅涉及 IoT 卡（设备必然有虚拟号，只有有→有一种情形）：

| 旧卡 | 新卡 | 行为 |
|------|------|------|
| 有虚拟号，pcd 有绑定 | 有虚拟号 | 更新 pcd 记录的 virtual_no 为新卡虚拟号 |
| 有虚拟号，pcd 有绑定 | 无虚拟号 | 禁用旧 pcd 记录（status=0）+ 创建新 pci ICCID 绑定 |
| 有虚拟号，pcd 无绑定 | 无虚拟号 | 跳过，无需迁移 |
| 无虚拟号，pci 有绑定 | 任意 | 迁移 pci 记录到新卡 ICCID（或新卡虚拟号路径） |
| 任意 | 任意（无绑定） | 跳过 |

### 修复换货服务

- `switchCustomerBindingWithTx` 替换为调用 `CustomerBinding.Migrate()`，移除函数内 `newKey == ""` 的误拦截逻辑
- `ensureNewAssetBindingAvailableWithTx` 更新：当新卡无虚拟号时，不再拦截换货，因为 `Migrate()` 已能正确处理此情形（无绑定时跳过，有绑定时迁移到 pci）

### 根本 bug 说明

原 `switchCustomerBindingWithTx` 在执行阶段做了 `if newKey == "" { return error }` 的检查，但没有先确认旧资产是否实际存在绑定记录。旧卡有虚拟号但无任何客户绑定时，也会被误拦截报错"新资产无法承接客户绑定"。

## Acceptance criteria

- [ ] 旧卡有虚拟号 + 有客户绑定，换货为有虚拟号新卡：pcd 记录的 virtual_no 正确更新为新卡虚拟号
- [ ] 旧卡有虚拟号 + 有客户绑定，换货为无虚拟号新卡：旧 pcd 记录 status 变为 0，pci 表出现新卡 ICCID 的绑定记录
- [ ] 旧卡有虚拟号 + 无客户绑定，换货为无虚拟号新卡：换货正常完成，不报错，不写入任何绑定记录
- [ ] 旧卡无虚拟号，换货为任意新卡：换货正常完成
- [ ] 有虚拟号换有虚拟号（原有场景）：行为与修复前一致，无回归
- [ ] 换货完成后，客户通过新卡（无论有无虚拟号）能正常通过归属验证

## Blocked by

`.scratch/customer-binding-architecture/issues/02-no-virtual-no-card-cend-flow.md`
