## Context

本变更将手机号卡套餐字段改为保号套餐，并建立“账号来源表维护、手机号卡展示同步结果”的模型。`wx_phone` 位于手机号独立数据源；企业微信资料 `wx_data` 位于主数据源，已包含 `phone_number` 与 `account`，可直接作为企业微信来源。

本期只新建抖音和微信来源表。企业微信复用 `wx_data`，不创建 `linked_wecom_account`，也不复制其维护逻辑。

## Goals / Non-Goals

### Goals

- `numberProtectionPackage` / `number_protection_package` 成为唯一套餐字段。
- `wechat` 成为微信账号文本，不再表示开通状态。
- 三类账号通过专用同步接口写入 `wx_phone`，普通保存无法修改。
- 同步支持选择记录、筛选结果和全部记录三种范围。
- 来源缺失或为空时保留手机号卡原账号。

### Non-Goals

- 不兼容 `packageChange5yuan`、旧 `wechat` 布尔值或旧数据。
- 不开发两张新来源表的维护页面和 CRUD 接口。
- 不创建企业微信来源表，不修改 `wx_data` 的现有维护接口。
- 不实现权限、审计、定时任务或外部平台拉取。

## Data Model

| 位置 | 列 | 类型 | 含义 |
|---|---|---|---|
| `wx_phone` | `number_protection_package` | `TINYINT(1) NOT NULL DEFAULT 0` | 保号套餐；`0` 未办理，`1` 已办理。 |
| `wx_phone` | `douyin_account` | `TEXT NULL` | 抖音账号同步展示结果。 |
| `wx_phone` | `wechat` | `TEXT NULL` | 微信账号同步展示结果。 |
| `wx_phone` | `linked_wecom` | `TEXT NULL` | 企业微信账号同步展示结果。 |
| 抖音来源 | `douyin_account` | `phone_number`、`douyin_account` | 一条记录存一个抖音账号。 |
| 微信来源 | `wechat_account` | `phone_number`、`wechat` | 一条记录存一个微信账号。 |
| 企业微信来源 | 现有 `wx_data` | `phone_number`、`account` | 一条记录存一个企业微信账号。 |

同一个手机号可在每张来源表中有多行；手机号卡不逐行复制，而是把同类账号合并成逗号文本用于展示。

## Sync Flow

```mermaid
flowchart TD
  A[管理员选择同步范围并确认] --> B[前端计算明确的手机号卡 ID 列表]
  B --> C[POST /api/wx-phones/account-sync]
  C --> D[逐张读取 wx_phone 手机号]
  D --> E[查询 douyin_account]
  E --> F[查询 wechat_account]
  F --> G[通过 WxDataMapper 查询 wx_data.account]
  G --> H{三类来源查询都成功?}
  H -->|否| I[该卡标记失败且不写入]
  H -->|是| J[去空格、忽略空值、去重并按 id 排序拼接]
  J --> K{任一来源有有效账号?}
  K -->|否| L[保留原账号并标记未更新]
  K -->|是| M[一次更新 wx_phone 三个结果字段]
  I --> N[处理下一张卡]
  L --> N
  M --> N
  N --> O[返回成功、未更新、失败汇总]
  O --> P[前端提示并刷新列表]
```

## API Design

普通 `POST /api/wx-phones` 与 `PUT /api/wx-phones/{id}` 只接受基础资料和 `numberProtectionPackage`。`douyinAccount`、`wechat`、`linkedWecom`、`packageChange5yuan` 均不是合法入参，传入时返回 400。

新增 `POST /api/wx-phones/account-sync`：

```json
{ "phoneIds": [101, 102] }
```

接口不支持缺少 `phoneIds` 时的隐式全量同步。前端必须显式计算并发送 ID，避免直接调用接口误同步所有手机号卡。

## Selection Rules

1. 有勾选：使用勾选 ID。
2. 无勾选且当前存在筛选：使用当前筛选结果的全部 ID。
3. 无勾选且无筛选：使用当前全量列表的全部 ID。
4. 同步前显示本次数量并要求确认；目标列表为空时不请求接口。

## Error Handling

- 单张卡来源查询失败：返回 `FAILED`，不写入该卡；批量继续。
- 三类来源均无有效值：返回 `UNCHANGED`，保留三个原值。
- 有一类或多类来源有有效值：仅覆盖有有效值的字段，其他字段保留原值；一次更新该手机号卡。
- 同步接口响应包含 `requestedCount`、`updatedCount`、`unchangedCount`、`failedCount` 和逐卡结果。

## Migration and Rollback

迁移由人工执行的预览库 SQL 完成。应用启动只校验最终结构；任何最终列或来源表缺失时停止启动并显示明确错误，不自动修改数据库。

回滚依赖迁移前 `wx_phone` 备份。删除旧 `wechat` 布尔列不能从新值自动还原，因此不提供猜测式回滚 SQL。

## Implementation Annotation Gate

所有实现方法必须以中文注释说明代码作用、关联文件和数据流，尤其是跨数据源读取、结果合并、空来源保留、失败不写入、目标 ID 计算与测试断言。