Files
junhong_cmp_fiber/migrations/000212_add_employee_collection_bills.up.sql
break ce24d5612e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m20s
feat(员工代收款): 新增员工代收款账单闭环
- 新增 6 张表与成对迁移 000212,扩展企业微信审批场景业务类型白名单
- 后台线下套餐订单与两条代理线下充值入账路径在来源成功事务内建账,来源唯一键幂等
- 核销申请、审批尝试记录、账单分摊预占与驳回重提,审批业务类型 employee_collection_approval
- 企业微信终态消费幂等:通过转已核销、驳回释放预占、通过后撤销不回滚并转异常终态
- 退款成功事务内按 bill_id+refund_id 幂等冲销账单或仅写退款关联提示
- 线下收款方式字典、账单查询/统计/关闭、申请查询与代办权限,均写入事务内审计

OpenSpec Change: add-employee-collection-bills
2026-09-10 18:24:05 +08:00

317 lines
22 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.
-- 员工代收款账单闭环:线下收款方式字典、账单、核销申请、审批尝试记录、分摊与退款冲销关联。
-- 不使用数据库外键;关联以 ID 保存并由应用层显式校验。
-- 建账只由来源成功事务携带的来源主键触发,来源唯一键兜底幂等,不提供历史扫描或补建路径。
-- 线下收款方式字典
CREATE TABLE tb_employee_collection_payment_method (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(64) NOT NULL,
name VARCHAR(100) NOT NULL,
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_employee_collection_payment_method_code CHECK (code <> ''),
CONSTRAINT chk_employee_collection_payment_method_name CHECK (name <> ''),
CONSTRAINT chk_employee_collection_payment_method_status CHECK (status IN (0, 1)),
CONSTRAINT chk_employee_collection_payment_method_sort CHECK (sort_order >= 0)
);
CREATE UNIQUE INDEX uk_employee_collection_payment_method_code
ON tb_employee_collection_payment_method (code)
WHERE deleted_at IS NULL;
CREATE INDEX idx_employee_collection_payment_method_status
ON tb_employee_collection_payment_method (status, sort_order, id)
WHERE deleted_at IS NULL;
-- 员工代收款账单
CREATE TABLE tb_employee_collection_bill (
id BIGSERIAL PRIMARY KEY,
source_type VARCHAR(20) NOT NULL,
source_id BIGINT NOT NULL,
source_key VARCHAR(64) NOT NULL,
source_no VARCHAR(64) NOT NULL DEFAULT '',
debtor_account_id BIGINT NOT NULL,
debtor_snapshot JSONB NOT NULL,
customer_snapshot JSONB NOT NULL,
receivable_amount BIGINT NOT NULL,
received_amount BIGINT NOT NULL DEFAULT 0,
reserved_amount BIGINT NOT NULL DEFAULT 0,
status SMALLINT NOT NULL DEFAULT 0,
closed_reason VARCHAR(500) NOT NULL DEFAULT '',
closed_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 uk_employee_collection_bill_source UNIQUE (source_key),
CONSTRAINT chk_employee_collection_bill_source_type CHECK (source_type IN ('order', 'recharge')),
CONSTRAINT chk_employee_collection_bill_source CHECK (source_id > 0 AND source_key <> ''),
CONSTRAINT chk_employee_collection_bill_debtor CHECK (debtor_account_id > 0 AND jsonb_typeof(debtor_snapshot) = 'object'),
CONSTRAINT chk_employee_collection_bill_customer CHECK (jsonb_typeof(customer_snapshot) = 'object'),
CONSTRAINT chk_employee_collection_bill_receivable CHECK (receivable_amount > 0),
CONSTRAINT chk_employee_collection_bill_amounts CHECK (
received_amount >= 0 AND reserved_amount >= 0
AND received_amount + reserved_amount <= receivable_amount
),
CONSTRAINT chk_employee_collection_bill_status CHECK (status IN (0, 1, 2, 3))
);
CREATE INDEX idx_employee_collection_bill_debtor
ON tb_employee_collection_bill (debtor_account_id, status, created_at, id);
CREATE INDEX idx_employee_collection_bill_status
ON tb_employee_collection_bill (status, created_at, id);
CREATE INDEX idx_employee_collection_bill_source
ON tb_employee_collection_bill (source_type, source_id);
-- 核销申请
CREATE TABLE tb_employee_collection_application (
id BIGSERIAL PRIMARY KEY,
applicant_account_id BIGINT NOT NULL,
acting_operator_id BIGINT NOT NULL DEFAULT 0,
acting_reason VARCHAR(500) NOT NULL DEFAULT '',
payment_method_id BIGINT NOT NULL,
payment_method_code VARCHAR(64) NOT NULL,
payment_method_name VARCHAR(100) NOT NULL,
paid_amount BIGINT NOT NULL,
payer_name VARCHAR(100) NOT NULL DEFAULT '',
paid_at TIMESTAMPTZ NOT NULL,
external_transaction_no VARCHAR(128) NOT NULL DEFAULT '',
payment_voucher_keys JSONB NOT NULL,
remark VARCHAR(500) NOT NULL DEFAULT '',
status SMALLINT NOT NULL DEFAULT 0,
latest_attempt_id BIGINT NOT NULL DEFAULT 0,
latest_approval_instance_id BIGINT NOT NULL DEFAULT 0,
decided_at TIMESTAMPTZ,
terminal_reason 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(),
CONSTRAINT chk_employee_collection_application_applicant CHECK (applicant_account_id > 0),
CONSTRAINT chk_employee_collection_application_method CHECK (
payment_method_id > 0 AND payment_method_code <> '' AND payment_method_name <> ''
),
CONSTRAINT chk_employee_collection_application_amount CHECK (paid_amount > 0),
CONSTRAINT chk_employee_collection_application_vouchers CHECK (
jsonb_typeof(payment_voucher_keys) = 'array' AND jsonb_array_length(payment_voucher_keys) >= 1
),
CONSTRAINT chk_employee_collection_application_acting CHECK (
acting_operator_id = 0 OR acting_reason <> ''
),
CONSTRAINT chk_employee_collection_application_status CHECK (status IN (0, 1, 2, 3))
);
CREATE INDEX idx_employee_collection_application_applicant
ON tb_employee_collection_application (applicant_account_id, status, id);
CREATE INDEX idx_employee_collection_application_status
ON tb_employee_collection_application (status, id);
CREATE INDEX idx_employee_collection_application_method
ON tb_employee_collection_application (payment_method_id, id);
-- 审批尝试记录:同一申请的每次提交或重提各持有独立审批实例,历史材料不可覆盖
CREATE TABLE tb_employee_collection_application_attempt (
id BIGSERIAL PRIMARY KEY,
application_id BIGINT NOT NULL,
attempt_no INTEGER NOT NULL,
payment_method_id BIGINT NOT NULL,
payment_method_code VARCHAR(64) NOT NULL,
payment_method_name VARCHAR(100) NOT NULL,
paid_amount BIGINT NOT NULL,
payer_name VARCHAR(100) NOT NULL DEFAULT '',
paid_at TIMESTAMPTZ NOT NULL,
external_transaction_no VARCHAR(128) NOT NULL DEFAULT '',
payment_voucher_keys JSONB NOT NULL,
remark VARCHAR(500) NOT NULL DEFAULT '',
submitted_by_account_id BIGINT NOT NULL,
acting_reason VARCHAR(500) NOT NULL DEFAULT '',
allocation_snapshot JSONB NOT NULL,
approval_instance_id BIGINT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_employee_collection_attempt_no UNIQUE (application_id, attempt_no),
CONSTRAINT chk_employee_collection_attempt_application CHECK (application_id > 0),
CONSTRAINT chk_employee_collection_attempt_no_positive CHECK (attempt_no >= 1),
CONSTRAINT chk_employee_collection_attempt_amount CHECK (paid_amount > 0),
CONSTRAINT chk_employee_collection_attempt_submitter CHECK (submitted_by_account_id > 0),
CONSTRAINT chk_employee_collection_attempt_vouchers CHECK (
jsonb_typeof(payment_voucher_keys) = 'array' AND jsonb_array_length(payment_voucher_keys) >= 1
),
CONSTRAINT chk_employee_collection_attempt_allocations CHECK (
jsonb_typeof(allocation_snapshot) = 'array' AND jsonb_array_length(allocation_snapshot) >= 1
),
CONSTRAINT chk_employee_collection_attempt_instance CHECK (
approval_instance_id IS NULL OR approval_instance_id > 0
)
);
CREATE UNIQUE INDEX uk_employee_collection_attempt_instance
ON tb_employee_collection_application_attempt (approval_instance_id)
WHERE approval_instance_id IS NOT NULL;
CREATE INDEX idx_employee_collection_attempt_application
ON tb_employee_collection_application_attempt (application_id, attempt_no DESC);
-- 申请—账单分摊:审批中预占,终态释放或转已核销
CREATE TABLE tb_employee_collection_application_allocation (
id BIGSERIAL PRIMARY KEY,
application_id BIGINT NOT NULL,
attempt_id BIGINT NOT NULL,
bill_id BIGINT NOT NULL,
amount BIGINT NOT NULL,
status SMALLINT NOT NULL DEFAULT 0,
released_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 uk_employee_collection_allocation_attempt_bill UNIQUE (attempt_id, bill_id),
CONSTRAINT chk_employee_collection_allocation_amount CHECK (amount > 0),
CONSTRAINT chk_employee_collection_allocation_status CHECK (status IN (0, 1, 2)),
CONSTRAINT chk_employee_collection_allocation_released CHECK ((status = 0) = (released_at IS NULL))
);
CREATE INDEX idx_employee_collection_allocation_bill
ON tb_employee_collection_application_allocation (bill_id, status, id);
CREATE INDEX idx_employee_collection_allocation_application
ON tb_employee_collection_application_allocation (application_id, attempt_id, id);
-- 退款冲销关联:同一退款对同一账单至多一条
CREATE TABLE tb_employee_collection_bill_refund (
id BIGSERIAL PRIMARY KEY,
bill_id BIGINT NOT NULL,
refund_id BIGINT NOT NULL,
source_order_id BIGINT NOT NULL,
refund_amount BIGINT NOT NULL,
bill_receivable_amount BIGINT NOT NULL,
outcome VARCHAR(20) NOT NULL,
reduced_amount BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_employee_collection_bill_refund UNIQUE (bill_id, refund_id),
CONSTRAINT chk_employee_collection_bill_refund_ids CHECK (
bill_id > 0 AND refund_id > 0 AND source_order_id > 0
),
CONSTRAINT chk_employee_collection_bill_refund_amount CHECK (
refund_amount > 0 AND bill_receivable_amount > 0 AND reduced_amount >= 0
),
CONSTRAINT chk_employee_collection_bill_refund_outcome CHECK (
outcome IN ('closed_full', 'reduced', 'hint_only')
),
CONSTRAINT chk_employee_collection_bill_refund_reduced CHECK (
outcome <> 'reduced' OR reduced_amount > 0
)
);
CREATE INDEX idx_employee_collection_bill_refund_bill
ON tb_employee_collection_bill_refund (bill_id, id);
-- 扩展企微审批场景业务类型白名单,纳入员工代收款核销审批;既有两个场景语义不变。
ALTER TABLE tb_wecom_approval_scene
DROP CONSTRAINT IF EXISTS chk_wecom_approval_scene_business;
ALTER TABLE tb_wecom_approval_scene
ADD CONSTRAINT chk_wecom_approval_scene_business
CHECK (business_type IN ('refund_approval', 'offline_recharge_approval', 'employee_collection_approval'));
COMMENT ON TABLE tb_employee_collection_payment_method IS '线下收款方式字典,被申请引用后只可停用';
COMMENT ON COLUMN tb_employee_collection_payment_method.id IS '主键收款方式ID';
COMMENT ON COLUMN tb_employee_collection_payment_method.code IS '稳定编码,未删除记录中唯一,被引用后不可修改';
COMMENT ON COLUMN tb_employee_collection_payment_method.name IS '收款方式名称';
COMMENT ON COLUMN tb_employee_collection_payment_method.sort_order IS '排序值,从 0 递增,最小为 0';
COMMENT ON COLUMN tb_employee_collection_payment_method.status IS '状态0 禁用、1 启用';
COMMENT ON COLUMN tb_employee_collection_payment_method.remark IS '备注';
COMMENT ON COLUMN tb_employee_collection_payment_method.creator IS '创建人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_payment_method.updater IS '最近更新人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_payment_method.created_at IS '创建时间';
COMMENT ON COLUMN tb_employee_collection_payment_method.updated_at IS '最近更新时间';
COMMENT ON COLUMN tb_employee_collection_payment_method.deleted_at IS '软删除时间,非空表示已删除;被引用记录不删除只停用';
COMMENT ON TABLE tb_employee_collection_bill IS '员工代收款账单,按来源唯一键至多一张,只由来源成功事务创建';
COMMENT ON COLUMN tb_employee_collection_bill.id IS '主键账单ID';
COMMENT ON COLUMN tb_employee_collection_bill.source_type IS '来源类型order 后台线下套餐订单、recharge 代理线下充值';
COMMENT ON COLUMN tb_employee_collection_bill.source_id IS '来源业务主键ID';
COMMENT ON COLUMN tb_employee_collection_bill.source_key IS '来源唯一键 order:{id} / recharge:{id},全局唯一';
COMMENT ON COLUMN tb_employee_collection_bill.source_no IS '来源单号快照';
COMMENT ON COLUMN tb_employee_collection_bill.debtor_account_id IS '欠款人后台账号ID创建后不可修改';
COMMENT ON COLUMN tb_employee_collection_bill.debtor_snapshot IS '欠款人账号名称与归属快照';
COMMENT ON COLUMN tb_employee_collection_bill.customer_snapshot IS '来源客户或店铺只读快照,不含敏感付款内容';
COMMENT ON COLUMN tb_employee_collection_bill.receivable_amount IS '应收金额(单位:分)';
COMMENT ON COLUMN tb_employee_collection_bill.received_amount IS '已核销金额(单位:分),仅企微最终通过的分摊增加';
COMMENT ON COLUMN tb_employee_collection_bill.reserved_amount IS '审批中预占金额(单位:分),防止并发申请超额核销';
COMMENT ON COLUMN tb_employee_collection_bill.status IS '状态0 待核销、1 部分核销、2 已核销、3 已关闭';
COMMENT ON COLUMN tb_employee_collection_bill.closed_reason IS '关闭原因,仅关闭时填写';
COMMENT ON COLUMN tb_employee_collection_bill.closed_at IS '关闭时间,空表示未关闭';
COMMENT ON COLUMN tb_employee_collection_bill.creator IS '创建人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_bill.updater IS '最近更新人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_bill.created_at IS '创建时间';
COMMENT ON COLUMN tb_employee_collection_bill.updated_at IS '最近更新时间';
COMMENT ON TABLE tb_employee_collection_application IS '核销申请按一笔外部付款一张申请只保存最新审批实例ID';
COMMENT ON COLUMN tb_employee_collection_application.id IS '主键申请ID';
COMMENT ON COLUMN tb_employee_collection_application.applicant_account_id IS '申请人后台账号ID';
COMMENT ON COLUMN tb_employee_collection_application.acting_operator_id IS '实际代办的超级管理员账号ID0 表示本人办理';
COMMENT ON COLUMN tb_employee_collection_application.acting_reason IS '代办原因,代办时必填';
COMMENT ON COLUMN tb_employee_collection_application.payment_method_id IS '线下收款方式字典ID';
COMMENT ON COLUMN tb_employee_collection_application.payment_method_code IS '收款方式稳定编码快照';
COMMENT ON COLUMN tb_employee_collection_application.payment_method_name IS '收款方式名称快照,字典改名不影响历史申请';
COMMENT ON COLUMN tb_employee_collection_application.paid_amount IS '人工确认的付款金额(单位:分)';
COMMENT ON COLUMN tb_employee_collection_application.payer_name IS '付款方名称';
COMMENT ON COLUMN tb_employee_collection_application.paid_at IS '付款时间';
COMMENT ON COLUMN tb_employee_collection_application.external_transaction_no IS '外部交易流水号,可由 OCR 预填但以人工确认值为准';
COMMENT ON COLUMN tb_employee_collection_application.payment_voucher_keys IS '支付凭证对象存储 Key 列表jsonb 存储 []string只保存对象键引用';
COMMENT ON COLUMN tb_employee_collection_application.remark IS '申请备注';
COMMENT ON COLUMN tb_employee_collection_application.status IS '状态0 审批中、1 已通过、2 已驳回、3 已撤销或已关闭';
COMMENT ON COLUMN tb_employee_collection_application.latest_attempt_id IS '最新审批尝试记录ID仅用于展示';
COMMENT ON COLUMN tb_employee_collection_application.latest_approval_instance_id IS '最新通用审批实例ID仅用于展示';
COMMENT ON COLUMN tb_employee_collection_application.decided_at IS '审批终态到达时间';
COMMENT ON COLUMN tb_employee_collection_application.terminal_reason IS '异常终态说明,如企微通过后撤销';
COMMENT ON COLUMN tb_employee_collection_application.creator IS '创建人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_application.updater IS '最近更新人用户ID0 表示系统';
COMMENT ON COLUMN tb_employee_collection_application.created_at IS '创建时间';
COMMENT ON COLUMN tb_employee_collection_application.updated_at IS '最近更新时间';
COMMENT ON TABLE tb_employee_collection_application_attempt IS '核销申请审批尝试记录,每次提交或重提新增一条不可变材料';
COMMENT ON COLUMN tb_employee_collection_application_attempt.id IS '主键审批尝试记录ID同时作为通用审批业务ID';
COMMENT ON COLUMN tb_employee_collection_application_attempt.application_id IS '所属核销申请ID';
COMMENT ON COLUMN tb_employee_collection_application_attempt.attempt_no IS '第几次提交,从 1 递增,申请内唯一';
COMMENT ON COLUMN tb_employee_collection_application_attempt.payment_method_id IS '本次提交冻结的收款方式字典ID';
COMMENT ON COLUMN tb_employee_collection_application_attempt.payment_method_code IS '本次提交冻结的收款方式编码';
COMMENT ON COLUMN tb_employee_collection_application_attempt.payment_method_name IS '本次提交冻结的收款方式名称';
COMMENT ON COLUMN tb_employee_collection_application_attempt.paid_amount IS '本次提交冻结的付款金额(单位:分)';
COMMENT ON COLUMN tb_employee_collection_application_attempt.payer_name IS '本次提交冻结的付款方名称';
COMMENT ON COLUMN tb_employee_collection_application_attempt.paid_at IS '本次提交冻结的付款时间';
COMMENT ON COLUMN tb_employee_collection_application_attempt.external_transaction_no IS '本次提交冻结的外部交易流水号';
COMMENT ON COLUMN tb_employee_collection_application_attempt.payment_voucher_keys IS '本次提交冻结的支付凭证对象键列表';
COMMENT ON COLUMN tb_employee_collection_application_attempt.remark IS '本次提交冻结的备注';
COMMENT ON COLUMN tb_employee_collection_application_attempt.submitted_by_account_id IS '本次实际提交账号ID代办时为超级管理员';
COMMENT ON COLUMN tb_employee_collection_application_attempt.acting_reason IS '本次代办原因,非代办为空';
COMMENT ON COLUMN tb_employee_collection_application_attempt.allocation_snapshot IS '本次提交冻结的账单分摊快照(账单摘要与金额)';
COMMENT ON COLUMN tb_employee_collection_application_attempt.approval_instance_id IS '本次尝试关联的通用审批实例ID创建后不可修改';
COMMENT ON COLUMN tb_employee_collection_application_attempt.created_at IS '创建时间';
COMMENT ON TABLE tb_employee_collection_application_allocation IS '核销申请对单张账单的本次分摊,审批中预占、终态释放或转已核销';
COMMENT ON COLUMN tb_employee_collection_application_allocation.id IS '主键分摊ID';
COMMENT ON COLUMN tb_employee_collection_application_allocation.application_id IS '所属核销申请ID';
COMMENT ON COLUMN tb_employee_collection_application_allocation.attempt_id IS '所属审批尝试记录ID审批中预占按此次尝试计算';
COMMENT ON COLUMN tb_employee_collection_application_allocation.bill_id IS '目标账单ID按账单ID升序加锁';
COMMENT ON COLUMN tb_employee_collection_application_allocation.amount IS '本次分摊金额(单位:分),必须大于 0';
COMMENT ON COLUMN tb_employee_collection_application_allocation.status IS '状态0 审批中预占、1 已通过转已核销、2 已驳回或已释放';
COMMENT ON COLUMN tb_employee_collection_application_allocation.released_at IS '预占释放时间,审批中为空';
COMMENT ON COLUMN tb_employee_collection_application_allocation.creator IS '创建人用户ID';
COMMENT ON COLUMN tb_employee_collection_application_allocation.updater IS '最近更新人用户ID';
COMMENT ON COLUMN tb_employee_collection_application_allocation.created_at IS '创建时间';
COMMENT ON COLUMN tb_employee_collection_application_allocation.updated_at IS '最近更新时间';
COMMENT ON TABLE tb_employee_collection_bill_refund IS '来源订单退款与账单的冲销关联,同一退款对同一账单至多一条';
COMMENT ON COLUMN tb_employee_collection_bill_refund.id IS '主键';
COMMENT ON COLUMN tb_employee_collection_bill_refund.bill_id IS '目标账单ID';
COMMENT ON COLUMN tb_employee_collection_bill_refund.refund_id IS '退款申请ID';
COMMENT ON COLUMN tb_employee_collection_bill_refund.source_order_id IS '账单来源订单ID';
COMMENT ON COLUMN tb_employee_collection_bill_refund.refund_amount IS '本次退款成功金额(单位:分)';
COMMENT ON COLUMN tb_employee_collection_bill_refund.bill_receivable_amount IS '冲销前的账单应收金额快照(单位:分)';
COMMENT ON COLUMN tb_employee_collection_bill_refund.outcome IS '处理结果closed_full 全额关闭、reduced 部分冲减、hint_only 仅退款关联提示';
COMMENT ON COLUMN tb_employee_collection_bill_refund.reduced_amount IS '实际冲减的应收金额(单位:分),仅 reduced 时大于 0';
COMMENT ON COLUMN tb_employee_collection_bill_refund.created_at IS '创建时间';