feat(业务用户组): AUG26-003 业务用户组与店铺负责人分组导入
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 1h43m42s

- 迁移 000221:新增 tb_business_user_group、tb_business_user_group_member、tb_shop_business_owner_import_task,成员一账号一行由部分唯一索引保证,店铺所属组按当前负责人实时推导,不回填历史分组。
- 用户组 CRUD、成员改组/清空归属、店铺批量交接(原子失败不部分写入)。
- 店铺负责人 CSV 导入任务:逐行独立事务、逐行明细、任务级与行级失败分离。
- 读侧推导与筛选:未分组、业务线、停用组可筛出并带停用标记。
- 补齐操作审计动作与资源、openapi 清单、发布门禁巡检表清单。
- 归档 add-shop-salesperson-groups 变更并同步 openspec/specs/business-user-group,补齐 AUG26-003 验证证据链。
This commit is contained in:
2026-09-14 16:51:44 +08:00
parent 957a235585
commit c7f9e005af
56 changed files with 4166 additions and 344 deletions

View File

@@ -0,0 +1,46 @@
-- 回滚业务用户组、成员归属与店铺负责人导入任务 Schema。
-- 三张表都承载不可由 Schema 重建的业务事实:用户组与成员关系决定店铺展示口径,
-- 导入任务保存逐行结果。存在任一记录时禁止破坏性回滚,需人工先核对并清理后再评估。
-- 注意:成员关系清空走软删,软删行同样计入本守卫,因此解除守卫需删除成员关系记录本身。
BEGIN;
-- 同一事务内先取排他锁再检查并删表,避免检查与删表之间写入新事实。
DO $$
BEGIN
IF to_regclass('tb_shop_business_owner_import_task') IS NOT NULL THEN
LOCK TABLE tb_shop_business_owner_import_task IN ACCESS EXCLUSIVE MODE;
IF EXISTS (SELECT 1 FROM tb_shop_business_owner_import_task LIMIT 1) THEN
RAISE EXCEPTION 'tb_shop_business_owner_import_task 已存在导入任务事实,禁止删表回滚,请停止导入后向前修复';
END IF;
END IF;
IF to_regclass('tb_business_user_group_member') IS NOT NULL THEN
LOCK TABLE tb_business_user_group_member IN ACCESS EXCLUSIVE MODE;
IF EXISTS (SELECT 1 FROM tb_business_user_group_member LIMIT 1) THEN
RAISE EXCEPTION 'tb_business_user_group_member 已存在平台用户分组事实(含软删行,清空归属走软删),禁止删表回滚,请删除成员关系记录后向前修复';
END IF;
END IF;
IF to_regclass('tb_business_user_group') IS NOT NULL THEN
LOCK TABLE tb_business_user_group IN ACCESS EXCLUSIVE MODE;
IF EXISTS (SELECT 1 FROM tb_business_user_group LIMIT 1) THEN
RAISE EXCEPTION 'tb_business_user_group 已存在业务用户组事实(含软删行),禁止删表回滚,请删除业务用户组记录后向前修复';
END IF;
END IF;
END
$$;
DROP INDEX IF EXISTS idx_shop_business_owner_import_task_status_created;
DROP INDEX IF EXISTS uq_shop_business_owner_import_task_no;
DROP TABLE IF EXISTS tb_shop_business_owner_import_task;
DROP INDEX IF EXISTS idx_business_user_group_member_group;
DROP INDEX IF EXISTS uk_business_user_group_member_account;
DROP TABLE IF EXISTS tb_business_user_group_member;
DROP INDEX IF EXISTS idx_business_user_group_status;
DROP INDEX IF EXISTS uk_business_user_group_code;
DROP TABLE IF EXISTS tb_business_user_group;
COMMIT;

View File

@@ -0,0 +1,134 @@
-- 业务用户组:平台用户的业务分类、成员归属与店铺负责人 CSV 导入任务。
-- 用户组只描述业务分类,不进入登录、角色权限或数据范围;店铺所属组始终由当前负责人实时推导,
-- 因此本迁移不给 tb_shop 增加任何组字段,也不回填历史分组。
-- 成员关系一账号一行,改组走 UPDATE 组 ID清空归属走软删与仓库统一 gorm.Model 基座一致),
-- 未删除记录的唯一性由部分唯一索引保证,历史归属变更由审计事件承担。
-- 导入任务独立成表,不复用设备导入任务表(后者的启动补偿按表绑定设备任务类型)。
-- 不使用数据库外键,关联以 ID 保存并由应用层显式校验。
CREATE TABLE tb_business_user_group (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
business_line VARCHAR(20) NOT NULL DEFAULT '',
sort_order BIGINT NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 1,
remark VARCHAR(500) NOT NULL DEFAULT '',
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_business_user_group_code CHECK (code <> ''),
CONSTRAINT chk_business_user_group_name CHECK (name <> ''),
CONSTRAINT chk_business_user_group_business_line CHECK (business_line IN ('', 'standard', 'smart', 'other')),
CONSTRAINT chk_business_user_group_status CHECK (status IN (0, 1)),
CONSTRAINT chk_business_user_group_sort CHECK (sort_order >= 0)
);
-- 稳定编码在未删除组内唯一;编码创建后不可修改,不存在跨组复用入口。
CREATE UNIQUE INDEX uk_business_user_group_code
ON tb_business_user_group (code)
WHERE deleted_at IS NULL;
CREATE INDEX idx_business_user_group_status
ON tb_business_user_group (status, sort_order, id)
WHERE deleted_at IS NULL;
CREATE TABLE tb_business_user_group_member (
id BIGSERIAL PRIMARY KEY,
business_user_group_id BIGINT NOT NULL,
account_id BIGINT NOT NULL,
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_business_user_group_member_group CHECK (business_user_group_id > 0),
CONSTRAINT chk_business_user_group_member_account CHECK (account_id > 0)
);
-- 一个账号至多一条未删除成员关系,唯一索引是「一账号至多一组」的结构性保证。
CREATE UNIQUE INDEX uk_business_user_group_member_account
ON tb_business_user_group_member (account_id)
WHERE deleted_at IS NULL;
-- 组维度用于删除前统计成员数。
CREATE INDEX idx_business_user_group_member_group
ON tb_business_user_group_member (business_user_group_id)
WHERE deleted_at IS NULL;
CREATE TABLE tb_shop_business_owner_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,
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_shop_business_owner_import_task_status CHECK (status IN (1, 2, 3, 4)),
CONSTRAINT chk_shop_business_owner_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_shop_business_owner_import_task_no
ON tb_shop_business_owner_import_task (task_no)
WHERE deleted_at IS NULL;
CREATE INDEX idx_shop_business_owner_import_task_status_created
ON tb_shop_business_owner_import_task (status, created_at DESC)
WHERE deleted_at IS NULL;
COMMENT ON TABLE tb_business_user_group IS '业务用户组,平台用户的业务分类,不进入鉴权与数据范围';
COMMENT ON COLUMN tb_business_user_group.id IS '主键';
COMMENT ON COLUMN tb_business_user_group.code IS '创建时必填的稳定编码,未删除组内唯一且创建后不可修改';
COMMENT ON COLUMN tb_business_user_group.name IS '用户组名称';
COMMENT ON COLUMN tb_business_user_group.business_line IS '所属业务线单值standard 标品、smart 智能产品、other 其他,空值表示未设置';
COMMENT ON COLUMN tb_business_user_group.sort_order IS '排序值,非负整数';
COMMENT ON COLUMN tb_business_user_group.status IS '状态 0-禁用 1-启用,与 pkg/constants.StatusDisabled/StatusEnabled 一致';
COMMENT ON COLUMN tb_business_user_group.remark IS '备注';
COMMENT ON COLUMN tb_business_user_group.creator IS '创建人账号ID';
COMMENT ON COLUMN tb_business_user_group.updater IS '最近更新人账号ID';
COMMENT ON COLUMN tb_business_user_group.created_at IS '创建时间';
COMMENT ON COLUMN tb_business_user_group.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_business_user_group.deleted_at IS '软删除时间,仅无成员组可删除';
COMMENT ON TABLE tb_business_user_group_member IS '平台用户与业务用户组的唯一归属关系';
COMMENT ON COLUMN tb_business_user_group_member.id IS '主键';
COMMENT ON COLUMN tb_business_user_group_member.business_user_group_id IS '所属业务用户组ID改组时直接更新本列';
COMMENT ON COLUMN tb_business_user_group_member.account_id IS '平台用户账号ID未删除记录内唯一';
COMMENT ON COLUMN tb_business_user_group_member.creator IS '创建人账号ID';
COMMENT ON COLUMN tb_business_user_group_member.updater IS '最近更新人账号ID';
COMMENT ON COLUMN tb_business_user_group_member.created_at IS '创建时间';
COMMENT ON COLUMN tb_business_user_group_member.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_business_user_group_member.deleted_at IS '软删除时间,清空归属走软删;唯一性由未删除记录的 partial 唯一索引保证';
COMMENT ON TABLE tb_shop_business_owner_import_task IS '店铺负责人 CSV 导入任务,逐行独立事务并保留逐行明细';
COMMENT ON COLUMN tb_shop_business_owner_import_task.id IS '主键';
COMMENT ON COLUMN tb_shop_business_owner_import_task.task_no IS '导入任务编号,未删除记录内唯一';
COMMENT ON COLUMN tb_shop_business_owner_import_task.file_name IS '上传的源 CSV 文件名';
COMMENT ON COLUMN tb_shop_business_owner_import_task.storage_key IS '源 CSV 对象存储Key';
COMMENT ON COLUMN tb_shop_business_owner_import_task.status IS '任务状态 1-待处理 2-处理中 3-已完成 4-失败';
COMMENT ON COLUMN tb_shop_business_owner_import_task.total_count IS '任务数据行总数,任务级失败时为 0';
COMMENT ON COLUMN tb_shop_business_owner_import_task.success_count IS '处理成功行数';
COMMENT ON COLUMN tb_shop_business_owner_import_task.fail_count IS '处理失败行数';
COMMENT ON COLUMN tb_shop_business_owner_import_task.result_items IS '逐行结果明细,行号自数据首行起计;任务级失败为空数组';
COMMENT ON COLUMN tb_shop_business_owner_import_task.error_message IS '任务级失败原因,与行级失败原因分开记录';
COMMENT ON COLUMN tb_shop_business_owner_import_task.creator_name IS '任务创建人名称快照';
COMMENT ON COLUMN tb_shop_business_owner_import_task.started_at IS '任务开始处理时间';
COMMENT ON COLUMN tb_shop_business_owner_import_task.completed_at IS '任务处理完成时间';
COMMENT ON COLUMN tb_shop_business_owner_import_task.creator IS '创建人账号ID';
COMMENT ON COLUMN tb_shop_business_owner_import_task.updater IS '最近更新人账号ID';
COMMENT ON COLUMN tb_shop_business_owner_import_task.created_at IS '创建时间';
COMMENT ON COLUMN tb_shop_business_owner_import_task.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_shop_business_owner_import_task.deleted_at IS '软删除时间';