feat(手机号资产关联): AUG26-009 手机号—资产关联、十项上限与后台解绑
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m2s
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:
25
migrations/000223_add_phone_asset_association.down.sql
Normal file
25
migrations/000223_add_phone_asset_association.down.sql
Normal file
@@ -0,0 +1,25 @@
|
||||
-- 回滚手机号—资产关联表。
|
||||
-- 失效关系行同样承载业务事实:审计只写脱敏手机号,完整手机号只能由关系行本身证明,
|
||||
-- 且批量解绑任务的逐行结果依赖解绑当时的关联快照,删表即不可重建。
|
||||
-- 因此存在任一记录(含已失效行)时禁止破坏性回滚,需人工核对后向前修复。
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 同一事务内先取排他锁再检查并删表,避免检查与删表之间写入新事实。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('tb_phone_asset_association') IS NOT NULL THEN
|
||||
LOCK TABLE tb_phone_asset_association IN ACCESS EXCLUSIVE MODE;
|
||||
IF EXISTS (SELECT 1 FROM tb_phone_asset_association LIMIT 1) THEN
|
||||
RAISE EXCEPTION 'tb_phone_asset_association 已存在手机号—资产关联事实(含已失效行),禁止删表回滚,请人工核对后向前修复';
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DROP INDEX IF EXISTS idx_phone_asset_association_asset;
|
||||
DROP INDEX IF EXISTS idx_phone_asset_association_phone;
|
||||
DROP INDEX IF EXISTS uq_phone_asset_association_valid;
|
||||
DROP TABLE IF EXISTS tb_phone_asset_association;
|
||||
|
||||
COMMIT;
|
||||
66
migrations/000223_add_phone_asset_association.up.sql
Normal file
66
migrations/000223_add_phone_asset_association.up.sql
Normal 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 并保留关系行';
|
||||
@@ -0,0 +1,23 @@
|
||||
-- 回滚手机号—资产关联 CSV 解绑导入任务表。
|
||||
-- 任务行保存逐行解绑结果与解绑当时的完整手机号快照,删表即不可重建,
|
||||
-- 因此存在任一记录(含软删行)时禁止破坏性回滚,需人工核对后向前修复。
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 同一事务内先取排他锁再检查并删表,避免检查与删表之间写入新事实。
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regclass('tb_phone_asset_unbind_import_task') IS NOT NULL THEN
|
||||
LOCK TABLE tb_phone_asset_unbind_import_task IN ACCESS EXCLUSIVE MODE;
|
||||
IF EXISTS (SELECT 1 FROM tb_phone_asset_unbind_import_task LIMIT 1) THEN
|
||||
RAISE EXCEPTION 'tb_phone_asset_unbind_import_task 已存在解绑导入任务事实(含软删行),禁止删表回滚,请人工核对后向前修复';
|
||||
END IF;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
DROP INDEX IF EXISTS idx_phone_asset_unbind_import_task_status_created;
|
||||
DROP INDEX IF EXISTS uq_phone_asset_unbind_import_task_no;
|
||||
DROP TABLE IF EXISTS tb_phone_asset_unbind_import_task;
|
||||
|
||||
COMMIT;
|
||||
63
migrations/000224_add_phone_asset_unbind_import_task.up.sql
Normal file
63
migrations/000224_add_phone_asset_unbind_import_task.up.sql
Normal file
@@ -0,0 +1,63 @@
|
||||
-- 手机号—资产关联 CSV 解绑导入任务。
|
||||
-- 仓库不存在统一导入任务框架,解绑导入沿用已有独立导入场景的做法:独立成表、独立队列,
|
||||
-- 与店铺负责人导入任务分开,避免复用其启动补偿与任务类型口径。
|
||||
-- 任务级解绑原因是任务自身的输入事实:Worker 载荷只带任务 ID,失效原因必须能从任务行读回。
|
||||
-- 逐行结果明细持久化解绑当时的完整手机号快照:解绑后关系立即失效,
|
||||
-- 只有快照能事后满足「批量任务结果展示完整手机号」;任务明细是业务事实表,按数据范围保护,不属于日志。
|
||||
-- 逐行独立事务,成功行提交、失败行保留原状,任务级失败不产生行明细,不设行数硬上限。
|
||||
-- 不使用数据库外键,关联以 ID 保存并由应用层显式校验。
|
||||
|
||||
CREATE TABLE tb_phone_asset_unbind_import_task (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
task_no VARCHAR(50) NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
storage_key VARCHAR(500) NOT NULL,
|
||||
unbind_reason VARCHAR(500) NOT NULL DEFAULT '',
|
||||
status INTEGER NOT NULL DEFAULT 1,
|
||||
total_count INTEGER NOT NULL DEFAULT 0,
|
||||
success_count INTEGER NOT NULL DEFAULT 0,
|
||||
fail_count INTEGER NOT NULL DEFAULT 0,
|
||||
result_items JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
creator_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
creator BIGINT NOT NULL DEFAULT 0,
|
||||
updater 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_unbind_import_task_status CHECK (status IN (1, 2, 3, 4)),
|
||||
CONSTRAINT chk_phone_asset_unbind_import_task_counts CHECK (
|
||||
total_count >= 0 AND success_count >= 0 AND fail_count >= 0
|
||||
AND success_count + fail_count <= total_count
|
||||
)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uq_phone_asset_unbind_import_task_no
|
||||
ON tb_phone_asset_unbind_import_task (task_no)
|
||||
WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_phone_asset_unbind_import_task_status_created
|
||||
ON tb_phone_asset_unbind_import_task (status, created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE tb_phone_asset_unbind_import_task IS '手机号—资产关联 CSV 解绑导入任务,逐行独立事务并保留逐行解绑手机号快照';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.id IS '主键';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.task_no IS '导入任务编号,未删除记录内唯一';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.file_name IS '上传的源 CSV 文件名';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.storage_key IS '源 CSV 对象存储Key';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.unbind_reason IS '任务级解绑原因,写入每次解除的失效原因与审计;行内不填原因';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.status IS '任务状态 1-待处理 2-处理中 3-已完成 4-失败';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.total_count IS '任务数据行总数,任务级失败时为 0';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.success_count IS '处理成功行数';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.fail_count IS '处理失败行数';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.result_items IS '逐行结果明细(含解绑当时的完整手机号快照),行号自数据首行起计;任务级失败为空数组';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.error_message IS '任务级失败原因,与行级失败原因分开记录';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.creator_name IS '任务创建人名称快照';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.started_at IS '任务开始处理时间';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.completed_at IS '任务处理完成时间';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.creator IS '创建人账号ID';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.updater IS '最近更新人账号ID';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.updated_at IS '最近更新时间';
|
||||
COMMENT ON COLUMN tb_phone_asset_unbind_import_task.deleted_at IS '软删除时间';
|
||||
Reference in New Issue
Block a user