Files
junhong_cmp_fiber/migrations/000215_add_withdrawal_qualification_and_attempt.up.sql
break 575d056f54
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(代理分销提现): 落地扫码注册、提现资料资格与企微终审提现
AUG26-008。

- 迁移 000214–000217:tb_shop 全局唯一且不可修改的随机分销码(含存量回填)、
  tb_agent_distribution_registration 待审批注册记录、tb_withdrawal_qualification 资料版本、
  tb_commission_withdrawal_request_attempt 审批尝试记录,以及提现申请的 latest_*/异常标记列;
  不修改既有迁移,down 在存在本 Change 业务事实或新类型场景行时拒绝破坏性回滚。
- 公开接口 POST /api/c/v1/agent-distribution-registrations:无认证,复用既有短信验证码校验、
  消费与限流;无效分销码、停用上级、验证码无效或已消费统一返回「分销码不可用」且不落库,
  审批通过前不创建店铺、账号或钱包。
- 审批通过才在同一事务内建启用店铺、代理主账号、钱包、上级层级与业务员快照,驳回不建实体,
  重复回调不重复建实体,提交后清理上级下级缓存。
- 提现资料资格按不可变版本保存,替换合同或法人身份证即新增版本并同事务失效旧有效版本;
  超管作废原因必填;代理停用与店铺删除联动失效。
- 提现每次提交或重提新增不可变审批尝试记录并冻结金额;企业微信通过仅一次从冻结扣减、
  保持状态 2 并写 paid_at(不使用状态 4),驳回/cancelled/deleted 仅一次释放,
  通过后撤销不回滚、不重新冻结、只写正交异常标记;加锁顺序统一为申请→尝试→钱包。
- 本地人工终审对已关联审批实例的申请返回状态冲突,approval_instance_id 为空的存量申请保持既有行为,
  不新增任何配置开关。
- 补齐审批业务类型注册点全集:业务类型与场景字段常量、场景 DTO 两处枚举与中文描述、
  场景字段白名单/合法类型/中文名、数据库 CHECK、Worker 决策消费者与装配、审批审计资源映射,
  以及三个新审计资源与 13 个审计动作;失败/拒绝审计改为必达。
- 新增后台路由与 OpenAPI:资格提交/查询/作废、提现申请/重提/详情、店铺详情返回只读分销码。
- 归档本 Change:主 Spec 新增 agent-distribution-withdrawal 能力(5 个 Requirement)。

验证(junhong_cmp_test + Redis DB 6,显式 DB_*,未重置整库):
- 迁移 up → version 217 且 dirty=false → down 3 → up 回 217,fixture 复核残留为 0。
- 受控状态机脚手架 227 项通过 / 0 项失败,覆盖 18 组场景(幂等与乱序回调、资金冻结/释放/重提、
  退款回扣 × 在途提现并发、负向场景拒绝审计与 14 个动作码审计真实落库)。
- gofmt 空、go build/go vet 通过、gendocs 与工作区逐字节一致、context-health 通过、
  openspec validate --strict 通过、doctor healthy;自动化测试按项目决策为 N/A。

运行期前置(未完成,非代码交付物):由超管经 PUT /api/admin/wecom/scenes/{business_type} 为
agent_distribution_approval、withdrawal_qualification_approval、commission_withdrawal_approval
配置启用场景与模板控件映射;未配置时相应提交失败关闭。
2026-09-14 09:45:13 +08:00

167 lines
12 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 提现资格资料版本与提现审批尝试记录。
-- 资格事实按版本不可变保存:替换合同或法人身份证即新增版本,旧有效版本在同一事务内失效。
-- 提现每次提交或重提新增一条不可变审批尝试记录,冻结与释放金额一律取尝试记录事实。
-- 附件字段每个只保存单个对象存储 Key必填合同、法人身份证正反面共 3 项,可选营业执照、门头照、发票共 3 项,
-- 合计不超过企业微信单张审批单 6 个附件上限。
-- 不使用数据库外键,关联以 ID 保存并由应用层显式校验。
CREATE TABLE tb_withdrawal_qualification (
id BIGSERIAL PRIMARY KEY,
shop_id BIGINT NOT NULL,
subject_type VARCHAR(20) NOT NULL,
subject_code VARCHAR(64) NOT NULL,
legal_person_id_card VARCHAR(64) NOT NULL,
contract_file_key VARCHAR(255) NOT NULL,
id_card_front_file_key VARCHAR(255) NOT NULL,
id_card_back_file_key VARCHAR(255) NOT NULL,
business_license_file_key VARCHAR(255) NOT NULL DEFAULT '',
shop_front_file_key VARCHAR(255) NOT NULL DEFAULT '',
invoice_file_key VARCHAR(255) NOT NULL DEFAULT '',
invoice_title VARCHAR(200) NOT NULL DEFAULT '',
invoice_subject_code VARCHAR(64) NOT NULL DEFAULT '',
status SMALLINT NOT NULL DEFAULT 0,
invalid_reason VARCHAR(500) NOT NULL DEFAULT '',
invalidated_at TIMESTAMPTZ,
invalidated_by BIGINT NOT NULL DEFAULT 0,
approval_instance_id BIGINT,
decided_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(),
CONSTRAINT chk_withdrawal_qualification_shop CHECK (shop_id > 0),
CONSTRAINT chk_withdrawal_qualification_subject CHECK (subject_type IN ('enterprise', 'personal')),
CONSTRAINT chk_withdrawal_qualification_subject_code CHECK (subject_code <> ''),
CONSTRAINT chk_withdrawal_qualification_legal_person CHECK (legal_person_id_card <> ''),
CONSTRAINT chk_withdrawal_qualification_attachments CHECK (
contract_file_key <> '' AND id_card_front_file_key <> '' AND id_card_back_file_key <> ''
),
CONSTRAINT chk_withdrawal_qualification_invoice CHECK (
invoice_file_key = '' OR (invoice_title <> '' AND invoice_subject_code <> '')
),
CONSTRAINT chk_withdrawal_qualification_status CHECK (status IN (0, 1, 2, 3)),
CONSTRAINT chk_withdrawal_qualification_invalidated CHECK ((status = 3) = (invalidated_at IS NOT NULL)),
CONSTRAINT chk_withdrawal_qualification_instance CHECK (
approval_instance_id IS NULL OR approval_instance_id > 0
)
);
-- 同一店铺至多一个有效版本;失效与新增在同一事务内完成,因此提交即可替换。
CREATE UNIQUE INDEX uk_withdrawal_qualification_valid
ON tb_withdrawal_qualification (shop_id)
WHERE status = 1;
CREATE INDEX idx_withdrawal_qualification_shop
ON tb_withdrawal_qualification (shop_id, status, id DESC);
CREATE UNIQUE INDEX uk_withdrawal_qualification_instance
ON tb_withdrawal_qualification (approval_instance_id)
WHERE approval_instance_id IS NOT NULL;
COMMENT ON TABLE tb_withdrawal_qualification IS '提现资料资格版本,不可变保存历史、附件引用与审批结果';
COMMENT ON COLUMN tb_withdrawal_qualification.id IS '主键资料版本ID同时作为通用审批业务ID';
COMMENT ON COLUMN tb_withdrawal_qualification.shop_id IS '所属代理店铺ID';
COMMENT ON COLUMN tb_withdrawal_qualification.subject_type IS '签约主体类型enterprise 企业、personal 个人';
COMMENT ON COLUMN tb_withdrawal_qualification.subject_code IS '签约主体代码:企业统一社会信用代码、个人身份证号(脱敏后对外展示)';
COMMENT ON COLUMN tb_withdrawal_qualification.legal_person_id_card IS '法人身份证号,个人代理与签约主体代码一致(脱敏后对外展示)';
COMMENT ON COLUMN tb_withdrawal_qualification.contract_file_key IS '合同附件对象存储 Key单个对象必填';
COMMENT ON COLUMN tb_withdrawal_qualification.id_card_front_file_key IS '法人身份证正面附件对象存储 Key单个对象必填';
COMMENT ON COLUMN tb_withdrawal_qualification.id_card_back_file_key IS '法人身份证反面附件对象存储 Key单个对象必填';
COMMENT ON COLUMN tb_withdrawal_qualification.business_license_file_key IS '营业执照附件对象存储 Key单个对象可选';
COMMENT ON COLUMN tb_withdrawal_qualification.shop_front_file_key IS '门头照附件对象存储 Key单个对象可选';
COMMENT ON COLUMN tb_withdrawal_qualification.invoice_file_key IS '发票附件对象存储 Key单个对象仅企业可选';
COMMENT ON COLUMN tb_withdrawal_qualification.invoice_title IS '发票抬头,仅企业填写,且必须与签约主体一致';
COMMENT ON COLUMN tb_withdrawal_qualification.invoice_subject_code IS '发票统一社会信用代码,必须与签约主体代码一致';
COMMENT ON COLUMN tb_withdrawal_qualification.status IS '状态0 待审批、1 已通过有效、2 已驳回、3 已失效或已作废';
COMMENT ON COLUMN tb_withdrawal_qualification.invalid_reason IS '失效原因,作废必填,代理停用联动失效时记录停用原因';
COMMENT ON COLUMN tb_withdrawal_qualification.invalidated_at IS '失效时间,状态为 3 时必填';
COMMENT ON COLUMN tb_withdrawal_qualification.invalidated_by IS '使资格失效的操作账号ID0 表示由店铺停用联动触发';
COMMENT ON COLUMN tb_withdrawal_qualification.approval_instance_id IS '关联的通用审批实例ID创建后不可修改';
COMMENT ON COLUMN tb_withdrawal_qualification.decided_at IS '审批终态到达时间';
COMMENT ON COLUMN tb_withdrawal_qualification.creator IS '创建人用户ID';
COMMENT ON COLUMN tb_withdrawal_qualification.updater IS '最近更新人用户ID';
COMMENT ON COLUMN tb_withdrawal_qualification.created_at IS '创建时间';
COMMENT ON COLUMN tb_withdrawal_qualification.updated_at IS '最近更新时间';
CREATE TABLE tb_commission_withdrawal_request_attempt (
id BIGSERIAL PRIMARY KEY,
request_id BIGINT NOT NULL,
attempt_no INTEGER NOT NULL,
amount BIGINT NOT NULL,
fee BIGINT NOT NULL DEFAULT 0,
fee_rate BIGINT NOT NULL DEFAULT 0,
actual_amount BIGINT NOT NULL DEFAULT 0,
withdrawal_method VARCHAR(20) NOT NULL,
account_info JSONB NOT NULL,
invoice_keys JSONB NOT NULL DEFAULT '[]'::jsonb,
submitted_by_account_id BIGINT NOT NULL,
approval_instance_id BIGINT,
released_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_commission_withdrawal_attempt_no UNIQUE (request_id, attempt_no),
CONSTRAINT chk_commission_withdrawal_attempt_ids CHECK (
request_id > 0 AND submitted_by_account_id > 0
),
CONSTRAINT chk_commission_withdrawal_attempt_no_positive CHECK (attempt_no >= 1),
CONSTRAINT chk_commission_withdrawal_attempt_amount CHECK (
amount > 0 AND fee >= 0 AND fee_rate >= 0 AND actual_amount >= 0
),
CONSTRAINT chk_commission_withdrawal_attempt_method CHECK (withdrawal_method <> ''),
CONSTRAINT chk_commission_withdrawal_attempt_account CHECK (jsonb_typeof(account_info) = 'object'),
CONSTRAINT chk_commission_withdrawal_attempt_invoice CHECK (jsonb_typeof(invoice_keys) = 'array'),
CONSTRAINT chk_commission_withdrawal_attempt_instance CHECK (
approval_instance_id IS NULL OR approval_instance_id > 0
)
);
CREATE UNIQUE INDEX uk_commission_withdrawal_attempt_instance
ON tb_commission_withdrawal_request_attempt (approval_instance_id)
WHERE approval_instance_id IS NOT NULL;
CREATE INDEX idx_commission_withdrawal_attempt_request
ON tb_commission_withdrawal_request_attempt (request_id, attempt_no DESC);
-- 未结算尝试是后续佣金回溯的释放接缝:按申请读取未释放尝试。
CREATE INDEX idx_commission_withdrawal_attempt_unreleased
ON tb_commission_withdrawal_request_attempt (request_id, id)
WHERE released_at IS NULL;
COMMENT ON TABLE tb_commission_withdrawal_request_attempt IS '提现审批尝试记录,每次提交或重提一条不可变材料与冻结事实';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.id IS '主键审批尝试记录ID同时作为通用审批业务ID';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.request_id IS '所属提现申请ID';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.attempt_no IS '第几次提交,从 1 递增,申请内唯一';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.amount IS '本次冻结的提现金额(单位:分),释放与扣减一律取该事实';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.fee IS '本次手续费快照(单位:分)';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.fee_rate IS '本次手续费率快照基点100=1%';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.actual_amount IS '本次实际到账金额快照(单位:分)';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.withdrawal_method IS '本次收款方式快照';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.account_info IS '本次收款账户信息快照jsonb 对象)';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.invoice_keys IS '本次申请级发票对象存储 Key 列表jsonb 数组)';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.submitted_by_account_id IS '本次实际提交账号ID';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.approval_instance_id IS '本次尝试关联的通用审批实例ID创建后不可修改';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.released_at IS '本次冻结释放时间,空表示冻结仍未结算;是回溯可重放的幂等依据';
COMMENT ON COLUMN tb_commission_withdrawal_request_attempt.created_at IS '创建时间';
ALTER TABLE tb_commission_withdrawal_request
ADD COLUMN IF NOT EXISTS latest_attempt_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS latest_approval_instance_id BIGINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS approval_instance_id BIGINT,
ADD COLUMN IF NOT EXISTS anomaly_flag SMALLINT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS anomaly_reason VARCHAR(500) NOT NULL DEFAULT '';
ALTER TABLE tb_commission_withdrawal_request
DROP CONSTRAINT IF EXISTS chk_commission_withdrawal_request_approval_instance;
ALTER TABLE tb_commission_withdrawal_request
ADD CONSTRAINT chk_commission_withdrawal_request_approval_instance
CHECK (approval_instance_id IS NULL OR approval_instance_id > 0);
ALTER TABLE tb_commission_withdrawal_request
DROP CONSTRAINT IF EXISTS chk_commission_withdrawal_request_anomaly;
ALTER TABLE tb_commission_withdrawal_request
ADD CONSTRAINT chk_commission_withdrawal_request_anomaly
CHECK (anomaly_flag IN (0, 1));
COMMENT ON COLUMN tb_commission_withdrawal_request.latest_attempt_id IS '最新审批尝试记录ID仅用于列表投影与历史事实定位';
COMMENT ON COLUMN tb_commission_withdrawal_request.latest_approval_instance_id IS '最新通用审批实例ID仅用于列表投影';
COMMENT ON COLUMN tb_commission_withdrawal_request.approval_instance_id IS '首次接入企业微信审批时关联的审批实例ID为空表示从未关联审批实例的存量申请仍走本地人工终审';
COMMENT ON COLUMN tb_commission_withdrawal_request.anomaly_flag IS '正交异常标记0 无异常、1 企业微信通过后撤销';
COMMENT ON COLUMN tb_commission_withdrawal_request.anomaly_reason IS '异常原因,供提现详情展示与人工处理';