## Context

一期资产后台的参考计划已定义 11 张空表及业务关联方向，但当前 Spring Boot + MyBatis-Plus 工程仍使用旧表，也没有 Flyway、Liquibase 或其他自动迁移机制。现有 `sql/phone-account-sync-preview-reset.sql` 是历史人工脚本，包含备份和删除操作，不能复用到本期。

本设计只准备可审阅的数据库结构交付物，目标库只在获得单独书面授权后由人工连接的 `xyw_data_test` 执行。用户提供的 `mch_ai_reply_rule` 是本期审计字段命名的参考：所有新表均带 `create_time DATETIME NULL`、`update_time DATETIME NULL` 和 `delete_time BIGINT NOT NULL DEFAULT 0`。

### 范围文件用途注释表

| 文件 | 文件用途 | 核心职责 | 上下游关联文件 | 主要关联逻辑 | 风险点 |
|---|---|---|---|---|---|
| `sql/asset-phase1-preflight.sql`（新增） | 只读预检脚本 | 显示当前库、11 表的存在状态及非目标库风险 | 建表脚本、验收记录 | 人工先读预检结果，再决定是否执行建表 | 误连库或发现部分表存在仍继续 |
| `sql/asset-phase1-schema.sql`（新增） | 一期建表脚本 | 创建 11 张空表、索引与表注释 | 字段字典、后续 Entity/Mapper | 授权后按固定顺序运行，仅含 `CREATE TABLE` | 字段或索引遗漏；DDL 失败后形成部分表 |
| `sql/asset-phase1-schema-verify.sql`（新增） | 只读结构验收脚本 | 核对表、列、主键、索引和禁止的约束 | 建表脚本、验收记录 | 建表完成后读取 `information_schema` 并人工判定 | 只检查表存在而漏掉关键列或索引 |
| `docs/acceptance/asset-phase1-schema-YYYY-MM-DD.md`（执行后新增） | 建表验收记录 | 记录授权、实际库名、执行时间、结果与异常 | 三份 SQL 脚本、授权记录 | 结构验收通过后归档证据 | 缺少可追溯证据，无法判断是否在授权库执行 |

## Goals / Non-Goals

**Goals:**

- 锁定 11 表的字段、主键、唯一规则和查询索引，供后续新后端的 Entity、Mapper、DTO 和 API 使用。
- 对所有表统一采用 `id`、`create_time`、`update_time`、`delete_time`；有效记录的 `delete_time` 固定为 `0`。
- 将建表拆为“只读预检 → 获权后人工建表 → 只读结构验收 → 留存记录”，并明确每一步的停止条件。
- 让同一业务主键可在软删除后重建：需要唯一的业务字段使用 `(业务字段, delete_time)` 复合唯一索引。

**Non-Goals:**

- 不连接数据库、不执行 SQL、不创建或删除任何实际表。
- 不修改旧表、旧接口、旧后端启动异常、前端页面、权限、认证或 API 文档。
- 不导入、迁移、回填或兼容历史数据；不创建外键、`REFERENCES`、`CHECK`、触发器或存储过程。
- 不定义 API 字段、DTO、Service 方法或 Entity；这些在后续新后端梯队再进行完整字段追踪。

## Decisions

### 1. 统一审计列与软删除列

每张表必须定义：

| 列 | 类型与默认值 | 规则 |
|---|---|---|
| `id` | `BIGINT UNSIGNED NOT NULL AUTO_INCREMENT` | 主键 |
| `create_time` | `DATETIME NULL` | 后续应用创建记录时写入；本期空表不依赖数据库默认值 |
| `update_time` | `DATETIME NULL` | 后续应用更新记录时写入；本期空表不依赖数据库自动更新行为 |
| `delete_time` | `BIGINT NOT NULL DEFAULT 0` | `0` 表示有效；软删除时由后续 Service 写入 Unix 毫秒时间戳 |

选择参考表的列名和空值策略，而不使用参考计划里的 `created_at` / `updated_at` 或 `ON UPDATE CURRENT_TIMESTAMP`。这样既满足最新命名约束，也避免不同 MySQL 版本对自动更新时间行为的差异。后续唯一查询必须带 `delete_time = 0`。

### 2. 11 张表与字段字典

所有表均使用 `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`，并在每项下附加上一节的 4 个通用列。

| 表 | 业务字段（除通用列） | 主键/唯一规则 | 查询索引 |
|---|---|---|---|
| `company_profile` | `company_name VARCHAR(128) NOT NULL`、`short_name VARCHAR(128)`、`unified_social_credit_code VARCHAR(64)`、`address VARCHAR(255)`、`contact_name VARCHAR(64)`、`contact_value VARCHAR(128)` | `id`；`uk_company_profile_name_delete_time (company_name, delete_time)` | 无额外索引 |
| `company_person` | `company_profile_id BIGINT UNSIGNED`、`person_name VARCHAR(64) NOT NULL`、`employment_status VARCHAR(32)`、`resigned_at DATETIME` | `id` | `idx_company_person_company_id_id (company_profile_id, id)`；`idx_company_person_employment_status (employment_status)` |
| `asset_device` | `device_name VARCHAR(128) NOT NULL`、`image_attachment_1 LONGTEXT`、`image_attachment_2 LONGTEXT`、`user_person_id BIGINT UNSIGNED`、`user_usage_status VARCHAR(32)`、`asset_relation_status VARCHAR(32)` | `id`；`uk_asset_device_name_delete_time (device_name, delete_time)` | `idx_asset_device_user_person_id`、`idx_asset_device_user_usage_status`、`idx_asset_device_relation_status` |
| `phone_asset` | `phone_number CHAR(11) NOT NULL`、`card_type VARCHAR(32)`、`iccid VARCHAR(32)`、`real_name_owner VARCHAR(64)`、`management_type VARCHAR(32)`、`disposal_status VARCHAR(32)`、`device_id BIGINT UNSIGNED`、`linked_wecom_accounts JSON`、`linked_wechat_accounts JSON`、`linked_douyin_accounts JSON`、`linked_domain_accounts JSON`、`linked_merchants JSON`、`relation_synced_at DATETIME` | `id`；`uk_phone_asset_phone_number_delete_time (phone_number, delete_time)` | `idx_phone_asset_card_type`、`idx_phone_asset_iccid`、`idx_phone_asset_device_id` |
| `wecom_account` | `wecom_name VARCHAR(128)`、`wecom_alias VARCHAR(128)`、`company_profile_id BIGINT UNSIGNED`、`wecom_account VARCHAR(128)`、`phone_asset_id BIGINT UNSIGNED`、`real_name_owner VARCHAR(64)`、`real_name_owner_status VARCHAR(32)`、`gender VARCHAR(16)`、`device_id BIGINT UNSIGNED`、`operator_person_id BIGINT UNSIGNED` | `id`；账号允许重复 | `idx_wecom_account_company_id`、`idx_wecom_account_phone_id_id (phone_asset_id, id)`、`idx_wecom_account_device_id`、`idx_wecom_account_operator_person_id`、`idx_wecom_account_name` |
| `wechat_account` | `wechat_id VARCHAR(128)`、`real_name_owner VARCHAR(64)`、`phone_asset_id BIGINT UNSIGNED`、`device_id BIGINT UNSIGNED`、`operator_person_id BIGINT UNSIGNED` | `id`；微信号允许重复 | `idx_wechat_account_wechat_id`、`idx_wechat_account_phone_id_id (phone_asset_id, id)`、`idx_wechat_account_device_id`、`idx_wechat_account_operator_person_id` |
| `douyin_account` | `douyin_id VARCHAR(128)`、`real_name_owner VARCHAR(64)`、`company_profile_id BIGINT UNSIGNED`、`phone_asset_id BIGINT UNSIGNED`、`device_id BIGINT UNSIGNED`、`operator_person_id BIGINT UNSIGNED` | `id`；抖音号允许重复 | `idx_douyin_account_douyin_id`、`idx_douyin_account_company_id`、`idx_douyin_account_phone_id_id (phone_asset_id, id)`、`idx_douyin_account_device_id`、`idx_douyin_account_operator_person_id` |
| `domain_account` | `account_identifier VARCHAR(128) NOT NULL`、`phone_asset_id BIGINT UNSIGNED` | `id`；`uk_domain_account_identifier_delete_time (account_identifier, delete_time)` | `idx_domain_account_phone_id_id (phone_asset_id, id)` |
| `domain_asset` | `domain_name VARCHAR(255) NOT NULL`、`domain_account_id BIGINT UNSIGNED`、`company_profile_id BIGINT UNSIGNED`、`expires_at DATETIME` | `id`；完整域名一期允许重复 | `idx_domain_asset_account_id_id (domain_account_id, id)`、`idx_domain_asset_company_id`、`idx_domain_asset_name`、`idx_domain_asset_expires_at` |
| `merchant` | `merchant_number VARCHAR(128) NOT NULL`、`company_profile_id BIGINT UNSIGNED`、`phone_asset_id BIGINT UNSIGNED` | `id`；`uk_merchant_number_delete_time (merchant_number, delete_time)` | `idx_merchant_company_id`、`idx_merchant_phone_id_id (phone_asset_id, id)` |
| `system_user` | `username VARCHAR(64) NOT NULL`、`password_hash VARCHAR(255) NOT NULL`、`role_code VARCHAR(32) NOT NULL`、`status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE'` | `id`；`uk_system_user_username_delete_time (username, delete_time)` | `idx_system_user_status`、`idx_system_user_role_code_status (role_code, status)` |

`JSON` 快照列不设置数据库默认值，以兼容 MySQL 的 JSON 默认值限制；后续 Service 创建 `phone_asset` 时必须显式传入 `[]`，同步失败时不得覆盖已有来源快照。图片字段保存地址或内容文本，不保存二进制数据。

### 3. 关系、约束与删除策略

字段名以 `*_id` 表示逻辑关联，但本期不建立数据库外键。这样能在后续多批次导入、独立 Service 校验和清理测试数据时避免 DDL 级阻塞；代价是后续新后端必须校验关联记录存在且 `delete_time = 0`，并在删除前阻止或处理仍被引用的记录。

不使用 `ENUM` 或 `CHECK` 限死状态取值。`employment_status`、`user_usage_status`、`asset_relation_status`、`card_type`、`management_type`、`disposal_status`、`real_name_owner_status`、`role_code` 和 `status` 均保存文本，由后续 Service 统一枚举校验。

### 4. 受控人工执行而非应用迁移

预检脚本只读取 `DATABASE()` 和 `information_schema`；当数据库名不是 `xyw_data_test`、任一目标表已存在或已存在部分目标表时，人工必须停止，不可继续。建表脚本不使用 `IF NOT EXISTS`，以避免把异常现状伪装为成功；每条 DDL 失败后立即停止，并运行验收脚本记录实际状态。

三份 SQL 文件的开头均需用中文说明：代码作用、关联文件、执行顺序、允许目标库、禁止行为与失败停止条件。它们不包含业务方法；因此本期没有 Java/JavaScript 方法注释需求。后续实现 Entity/Mapper/Service 时，改动范围内每个方法再补“代码作用（白话）/关联文件/关联逻辑（调用链、消息链或数据流）”注释。

### 5. 备选方案与取舍

| 方案 | 结论 | 原因 |
|---|---|---|
| 引入 Flyway/Liquibase 自动迁移 | 不采用 | 当前仅建空表、尚未授权连接库；引入依赖和自动执行会扩大风险与范围 |
| 复用历史重建脚本 | 不采用 | 该脚本包含备份、`DROP` 与旧表，违反本期禁止删除和旧表读取的边界 |
| 数据库外键/检查约束 | 不采用 | 参考计划已指定由后续 Service 做关联、枚举与容量校验 |
| 使用 `created_at`/`updated_at` | 不采用 | 用户最新通用字段约束明确为 `create_time`/`update_time` |
| 为 JSON 快照设数据库默认 `[]` | 不采用 | 存在 MySQL 版本差异；由后续 Service 显式写入空数组更可控 |

## Risks / Trade-offs

| 风险 | 缓解措施 |
|---|---|
| 误连非测试库 | 预检先显示 `DATABASE()`；发现非 `xyw_data_test` 立即停止，验收记录保留实际库名 |
| 任一 DDL 失败形成部分表 | 预检要求 11 表全不存在；发生错误即停止并运行只读验收，不自动补表或删除 |
| 软删除查询漏加过滤条件 | 后续 Entity/Mapper/Service 统一以 `delete_time = 0` 查询有效数据，并以 API 回归测试覆盖 |
| JSON 空值或无效内容 | 表结构允许 `NULL` 以保证数据库兼容；新后端写入必须显式 `[]` 并校验 JSON 结构 |
| 无外键导致孤儿关联 | 后续 Service 在创建、更新、删除时校验逻辑关联和引用关系；一期结构验收仅确认无外键 |
| 复合唯一索引中的删除时间冲突 | 软删除操作统一写 Unix 毫秒时间戳；若实际删除实现无法保证该粒度，后续必须改用单调值后再上线 |

### 工程术语注释表

| 术语 | 白话解释 | 使用位置 | 不这样做的风险 | 示例 |
|---|---|---|---|---|
| 软删除（不直接删行，而是标记已删除） | 保留旧记录，把 `delete_time` 从 `0` 改为删除时刻 | 所有 11 表 | 直接删除会丢失追溯线索，也无法安全恢复 | 有效手机号 `delete_time=0`；删除后写入毫秒时间戳 |
| 复合唯一索引（两个字段合起来不能重复） | 数据库把业务键和删除标记一起判断是否重复 | 需去重的业务表 | 仅按业务键唯一会阻止软删除后重新创建；不唯一会产生多条有效重复数据 | `phone_number + delete_time` |
| 预检（先只读查看再操作） | 先确认连的是哪一个库、表是否为空 | `asset-phase1-preflight.sql` | 误库执行或在半成品结构上继续会扩大事故 | `SELECT DATABASE()` 后检查 11 表 |
| 回滚（失败后恢复到安全状态） | 本期不自动删除表，而是停止后记录现状，等待新的授权修订方案 | DDL 失败处理 | 自动 `DROP` 可能误删不在范围的数据 | 验收脚本显示已创建表，后续走单独审批 |
| 幂等（重复执行结果仍可预测） | 本期建表脚本故意不追求静默重复执行，重复时必须报错暴露异常现状 | 建表脚本 | `IF NOT EXISTS` 会掩盖表结构不一致 | 已存在 `phone_asset` 时 `CREATE TABLE` 失败并停止 |

## Migration Plan

1. 审批本变更的字段字典和 SQL 范围；在此之前不生成或执行实际 SQL。
2. 获得对 `xyw_data_test` 的单独书面建表授权，并记录可恢复备份位置和执行人。
3. 人工连接目标库，执行只读预检；任一停止条件命中即结束。
4. 仅在预检全部通过后，按 `company_profile → company_person → asset_device → phone_asset → wecom_account / wechat_account / douyin_account / domain_account / merchant → domain_asset → system_user` 执行建表脚本。
5. 立即执行只读结构验收，检查 11 表、通用列、业务列、主键、唯一/查询索引以及“无外键、无检查约束”。
6. 在验收记录中写明实际库名、授权、时间、脚本版本、PASS/FAIL 和异常；仅验收通过才进入后续新后端梯队。

本期没有自动回滚 SQL。若失败：停止后续 DDL/开发，保留现状和验收输出；任何修订、删除或重建动作均须由新的明确计划和单独数据库授权覆盖。

## Open Questions

- 参考计划已给出一期最小字段，本提案据此锁定。若业务需要人员手机号/岗位/备注，或变更 `management_type`、`disposal_status` 的正式枚举，应在建表前作为新的字段变更审批，不能在执行时临时添加。
- `system_user` 仅建结构，不创建管理员账号或密码摘要；初始账户策略属于后续认证梯队。
