feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s

- 新增成对迁移 000223(tb_phone_asset_association,含有效关系部分唯一索引与 down 守卫)与 000224(解绑导入任务表),不回填历史
- H5:need_bind_phone 三支判定(开关关闭完全短路);已有主号幂等建联;十项上限按手机号 advisory 串行化(含换绑到全新号的并发场景);换绑原子迁移与冲突整单回滚;不写遗留列
- 后台:关联列表、单项/批量解绑、CSV 导入解绑(B1–B16),超管/平台 gate + 资产数据范围复核,三态统一文案
- 读侧:卡/设备列表与详情按页一次 IN 聚合;两类导出补「关联手机号」列并保留历史表头反解兼容
- 脱敏:关联审计走独立动作/资源只写脱敏手机号;访问日志手机号类字段脱敏
- 同步主 Spec openspec/specs/phone-asset-association 并归档 AUG26-009,补齐 requirement-evidence 与入口矩阵,context-health 通过
This commit is contained in:
2026-09-15 11:54:56 +08:00
parent 93e072e1e2
commit 70e680eb0a
67 changed files with 3951 additions and 147 deletions

View File

@@ -0,0 +1,66 @@
-- 手机号—资产当前有效关联。
-- 既有事实只有「客户↔手机号」与「客户↔资产」两条互不相交的链路,无法表达单项资产的验证关系,
-- 后台查看、按资产解除、换绑迁移与批量解绑都缺少可依据的关联事实。
-- 关联只能由 H5 短信验证建立source 固定 h5_sms_verification后台不提供创建或补录入口。
-- iot_card 与 device 的 ID 空间独立,资产身份一律取 (asset_type, asset_id),同号互不覆盖也不合并计数。
-- 失效走 status 置 0 并同时写入失效时间、方式与操作人,关系行本身保留为业务事实,不做物理或软删除。
-- 单手机号最多 10 项有效关系,上限只统计 status = 1 的行;本迁移不回填任何历史关系。
-- 不使用数据库外键,关联以 ID 保存并由应用层显式校验。
CREATE TABLE tb_phone_asset_association (
id BIGSERIAL PRIMARY KEY,
phone VARCHAR(20) NOT NULL,
asset_type VARCHAR(20) NOT NULL,
asset_id BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 1,
source VARCHAR(30) NOT NULL DEFAULT 'h5_sms_verification',
established_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
invalidated_at TIMESTAMPTZ,
invalidation_method VARCHAR(30) NOT NULL DEFAULT '',
invalidation_reason VARCHAR(500) NOT NULL DEFAULT '',
invalidator BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT chk_phone_asset_association_phone CHECK (phone <> ''),
CONSTRAINT chk_phone_asset_association_asset_type CHECK (asset_type IN ('iot_card', 'device')),
CONSTRAINT chk_phone_asset_association_asset CHECK (asset_id > 0),
CONSTRAINT chk_phone_asset_association_status CHECK (status IN (0, 1)),
CONSTRAINT chk_phone_asset_association_source CHECK (source = 'h5_sms_verification'),
-- 有效关系不得带失效信息,已失效关系必须带失效时间:两者互为充要条件,避免出现半失效状态。
CONSTRAINT chk_phone_asset_association_invalidation CHECK (
(status = 1 AND invalidated_at IS NULL)
OR (status = 0 AND invalidated_at IS NOT NULL)
)
);
-- 同一手机号与同一 (资产类型, 资产ID) 同时只能有一条有效关系。
-- 部分唯一索引既是幂等建联的结构性保证,也是并发同资产建联的兜底:冲突由应用映射为幂等成功。
-- 迁移与「新号已存在的同资产有效关系」冲突时由本索引报错,换绑整次回滚。
CREATE UNIQUE INDEX uq_phone_asset_association_valid
ON tb_phone_asset_association (phone, asset_type, asset_id)
WHERE status = 1 AND deleted_at IS NULL;
-- 十项上限按手机号计数,计数只读有效关系。
CREATE INDEX idx_phone_asset_association_phone
ON tb_phone_asset_association (phone, status)
WHERE deleted_at IS NULL;
-- 列表、详情与两类导出按本批资产集合一次 IN 批量聚合,禁止逐资产查询。
CREATE INDEX idx_phone_asset_association_asset
ON tb_phone_asset_association (asset_type, asset_id, status)
WHERE deleted_at IS NULL;
COMMENT ON TABLE tb_phone_asset_association IS '手机号—资产当前有效关联,仅由 H5 短信验证建立,单手机号最多 10 项有效关系';
COMMENT ON COLUMN tb_phone_asset_association.id IS '主键';
COMMENT ON COLUMN tb_phone_asset_association.phone IS '已验证手机号,计数与唯一口径均以本列为准';
COMMENT ON COLUMN tb_phone_asset_association.asset_type IS '资产类型 iot_card-物联网卡 device-设备,与 pkg/constants.AssetTypeIotCard/AssetTypeDevice 一致';
COMMENT ON COLUMN tb_phone_asset_association.asset_id IS '资产ID与 asset_type 共同构成资产身份两类资产ID空间独立';
COMMENT ON COLUMN tb_phone_asset_association.status IS '状态 0-已失效 1-有效10 项上限只统计有效关系';
COMMENT ON COLUMN tb_phone_asset_association.source IS '建立来源,固定为 h5_sms_verification后台无创建或补录入口';
COMMENT ON COLUMN tb_phone_asset_association.established_at IS '关联建立时间,即短信验证通过时间';
COMMENT ON COLUMN tb_phone_asset_association.invalidated_at IS '失效时间,仅 status = 0 时非空';
COMMENT ON COLUMN tb_phone_asset_association.invalidation_method IS '失效方式 backend_single-后台单项 backend_batch-后台批量 csv_import-CSV导入';
COMMENT ON COLUMN tb_phone_asset_association.invalidation_reason IS '失效原因后台解除必填最长500字符';
COMMENT ON COLUMN tb_phone_asset_association.invalidator IS '失效操作人账号ID系统任务为 0';
COMMENT ON COLUMN tb_phone_asset_association.created_at IS '创建时间';
COMMENT ON COLUMN tb_phone_asset_association.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_phone_asset_association.deleted_at IS '软删除时间,本表不使用:失效走 status 置 0 并保留关系行';