-- 沉默期延期政策、沉默期流水、延期订单、延期批次、延期明细(add-package-silence-period,技术方案 §3.4、§3.6、§7)。 -- -- 设计选择: -- 1. 延期政策按"指定套餐 > 套餐系列 > 全部套餐"匹配;同一优先级适用范围与生效时间段不得重叠,由应用层保存时校验。 -- 2. 沉默期流水只新增不修改:设置、调整、过期、结束、延期、换货迁移、回填、实名时间补写。 -- 3. 延期订单是交易记录(在线 H5、后台二维码、线下登记),支付单复用 tb_payment(order_type = silence_extension);交易记录不删除。 -- 4. 每次申请都建一个批次:online 在线付费 / offline 线下付费 / special 特殊延期 / adjust 购买时调整沉默期。 -- 企微审批实例统一关联批次(审批业务类型只有 package_silence_approval,business_id = 批次 ID)。 -- 5. 延期明细是延期事实,以明细状态条件更新保证重复回调、重复审批通过只执行一次。 -- 6. 订单、批次、明细都冻结政策快照;改政策只影响之后发起的延期。 -- 不使用数据库外键(ENG-MODEL-001)。 CREATE TABLE tb_package_silence_extension_policy ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, scope_type VARCHAR(20) NOT NULL, scope_ids JSONB NOT NULL DEFAULT '[]', unit_price BIGINT NOT NULL, free_extension_limit INTEGER NOT NULL DEFAULT 1, paid_max_months INTEGER NOT NULL DEFAULT 6, single_max_months INTEGER, approval_exempt_types JSONB NOT NULL DEFAULT '[]', effective_from TIMESTAMPTZ NOT NULL, effective_to TIMESTAMPTZ, 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 ck_silence_policy_scope_type CHECK (scope_type IN ('all', 'series', 'package')), CONSTRAINT ck_silence_policy_status CHECK (status IN (0, 1)), CONSTRAINT ck_silence_policy_unit_price CHECK (unit_price >= 0), CONSTRAINT ck_silence_policy_free_limit CHECK (free_extension_limit >= 0), CONSTRAINT ck_silence_policy_paid_max CHECK (paid_max_months >= 0), CONSTRAINT ck_silence_policy_single_max CHECK (single_max_months IS NULL OR single_max_months >= 1), CONSTRAINT ck_silence_policy_effective CHECK (effective_to IS NULL OR effective_to > effective_from) ); CREATE UNIQUE INDEX uq_silence_policy_name ON tb_package_silence_extension_policy (name) WHERE deleted_at IS NULL; CREATE INDEX idx_silence_policy_match ON tb_package_silence_extension_policy (scope_type, status) WHERE deleted_at IS NULL; COMMENT ON TABLE tb_package_silence_extension_policy IS '沉默期延期政策:适用范围、单价、免费次数、付费上限、免审批场景、生效时间、启停'; COMMENT ON COLUMN tb_package_silence_extension_policy.scope_type IS '适用范围:all-全部套餐 series-套餐系列 package-指定套餐'; COMMENT ON COLUMN tb_package_silence_extension_policy.scope_ids IS '系列 ID 或套餐 ID 列表(all 时为空数组)'; COMMENT ON COLUMN tb_package_silence_extension_policy.unit_price IS '延期单价(分/月,1 月 = 30 天)'; COMMENT ON COLUMN tb_package_silence_extension_policy.free_extension_limit IS '每笔套餐免费延期次数'; COMMENT ON COLUMN tb_package_silence_extension_policy.paid_max_months IS '每笔套餐付费延期累计最多月数'; COMMENT ON COLUMN tb_package_silence_extension_policy.single_max_months IS '单次付费延期最多月数(空表示不单独限制)'; COMMENT ON COLUMN tb_package_silence_extension_policy.approval_exempt_types IS '免审批场景(多选):offline_paid/free/discount/complaint_compensation/system_compensation/special_customer/batch/purchase_adjust;空数组表示全部需审批'; COMMENT ON COLUMN tb_package_silence_extension_policy.effective_from IS '生效时间'; COMMENT ON COLUMN tb_package_silence_extension_policy.effective_to IS '失效时间(空表示长期)'; COMMENT ON COLUMN tb_package_silence_extension_policy.status IS '状态:0-停用 1-启用'; CREATE TABLE tb_package_silence_record ( id BIGSERIAL PRIMARY KEY, package_usage_id BIGINT NOT NULL, event_type VARCHAR(30) NOT NULL, trigger_source VARCHAR(40) NOT NULL DEFAULT '', asset_type VARCHAR(20) NOT NULL DEFAULT '', asset_id BIGINT NOT NULL DEFAULT 0, before_status VARCHAR(16), after_status VARCHAR(16), before_deadline_at TIMESTAMPTZ, after_deadline_at TIMESTAMPTZ, period_days INTEGER, default_days_snapshot INTEGER, is_adjusted BOOLEAN NOT NULL DEFAULT FALSE, change_days INTEGER NOT NULL DEFAULT 0, operator_id BIGINT NOT NULL DEFAULT 0, operator_type VARCHAR(20) NOT NULL DEFAULT '', reason VARCHAR(500) NOT NULL DEFAULT '', approval_instance_id BIGINT, batch_id BIGINT, extension_id BIGINT, exchange_order_id BIGINT, exchange_no VARCHAR(64) NOT NULL DEFAULT '', realname_at TIMESTAMPTZ, extra JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT ck_silence_record_event_type CHECK (event_type IN ( 'set', 'adjust', 'expired', 'ended', 'extend', 'exchange', 'backfill', 'realname_backfill' )) ); CREATE INDEX idx_silence_record_usage ON tb_package_silence_record (package_usage_id, id); CREATE INDEX idx_silence_record_created ON tb_package_silence_record (created_at DESC); COMMENT ON TABLE tb_package_silence_record IS '套餐沉默期变更流水(只新增不修改)'; COMMENT ON COLUMN tb_package_silence_record.event_type IS '事件:set-设置 adjust-调整审批结束 expired-过期 ended-期内实名结束 extend-延期 exchange-换货迁移 backfill-历史回填 realname_backfill-补写实名时间'; COMMENT ON COLUMN tb_package_silence_record.trigger_source IS '触发来源(订单支付、自动购包、实名、扫描、延期、审批、换货、回填等)'; COMMENT ON COLUMN tb_package_silence_record.default_days_snapshot IS '套餐默认沉默期快照'; COMMENT ON COLUMN tb_package_silence_record.is_adjusted IS '本次是否使用了购买时调整值'; COMMENT ON COLUMN tb_package_silence_record.change_days IS '本次变化天数(延期天数或调整后与默认值差额)'; COMMENT ON COLUMN tb_package_silence_record.operator_type IS '操作人类型:system/account/customer'; COMMENT ON COLUMN tb_package_silence_record.realname_at IS '判定所用的实名时间'; CREATE TABLE tb_package_silence_extension_batch ( id BIGSERIAL PRIMARY KEY, batch_no VARCHAR(40) NOT NULL, path VARCHAR(20) NOT NULL, extension_type VARCHAR(30) NOT NULL, approval_required BOOLEAN NOT NULL DEFAULT FALSE, reason VARCHAR(1000) NOT NULL DEFAULT '', evidence_refs JSONB NOT NULL DEFAULT '[]', basis VARCHAR(1000) NOT NULL DEFAULT '', import_file_key VARCHAR(500) NOT NULL DEFAULT '', policy_id BIGINT, policy_name_snapshot VARCHAR(100) NOT NULL DEFAULT '', unit_price_snapshot BIGINT NOT NULL DEFAULT 0, approval_exempt_types_snapshot JSONB NOT NULL DEFAULT '[]', free_extension_limit_snapshot INTEGER NOT NULL DEFAULT 1, paid_max_months_snapshot INTEGER NOT NULL DEFAULT 0, single_max_months_snapshot INTEGER, adjust_package_usage_id BIGINT, adjust_default_days INTEGER, adjust_requested_days INTEGER, amount BIGINT NOT NULL DEFAULT 0, offline_payment_method_id BIGINT, offline_payment_method_code VARCHAR(64), offline_payment_method_name VARCHAR(100), offline_trade_no VARCHAR(128), offline_paid_amount BIGINT, offline_paid_at TIMESTAMPTZ, payment_voucher_keys JSONB NOT NULL DEFAULT '[]', requested_by BIGINT NOT NULL DEFAULT 0, requested_by_type VARCHAR(20) NOT NULL DEFAULT '', requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), approval_instance_id BIGINT, approval_status VARCHAR(20) NOT NULL DEFAULT '', approval_finished_at TIMESTAMPTZ, status VARCHAR(30) NOT NULL, requested_count INTEGER NOT NULL DEFAULT 0, succeeded_count INTEGER NOT NULL DEFAULT 0, failed_count INTEGER NOT NULL DEFAULT 0, completed_at TIMESTAMPTZ, shop_id_tag BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT ck_silence_batch_path CHECK (path IN ('online', 'offline', 'special', 'adjust')), CONSTRAINT ck_silence_batch_type CHECK (extension_type IN ( 'standard_paid', 'offline_paid', 'free', 'discount', 'complaint_compensation', 'system_compensation', 'special_customer', 'batch', 'purchase_adjust' )), CONSTRAINT ck_silence_batch_status CHECK (status IN ( 'pending_payment', 'pending_approval', 'executing', 'completed', 'partially_completed', 'rejected', 'closed', 'failed' )) ); CREATE UNIQUE INDEX uq_silence_batch_no ON tb_package_silence_extension_batch (batch_no); CREATE UNIQUE INDEX uq_silence_batch_approval ON tb_package_silence_extension_batch (approval_instance_id) WHERE approval_instance_id IS NOT NULL; CREATE UNIQUE INDEX uq_silence_batch_offline_trade ON tb_package_silence_extension_batch (offline_payment_method_id, offline_trade_no) WHERE offline_trade_no IS NOT NULL AND path = 'special' AND status NOT IN ('rejected', 'closed'); CREATE UNIQUE INDEX uq_silence_batch_adjust_open ON tb_package_silence_extension_batch (adjust_package_usage_id) WHERE path = 'adjust' AND status = 'pending_approval'; CREATE INDEX idx_silence_batch_list ON tb_package_silence_extension_batch (path, status, created_at DESC); COMMENT ON TABLE tb_package_silence_extension_batch IS '沉默期申请批次:每次延期或购买时调整都建一个批次,企微审批实例关联批次'; COMMENT ON COLUMN tb_package_silence_extension_batch.path IS '路径:online-在线付费 offline-线下付费 special-特殊延期 adjust-购买时调整沉默期'; COMMENT ON COLUMN tb_package_silence_extension_batch.extension_type IS '类型:standard_paid/offline_paid/free/discount/complaint_compensation/system_compensation/special_customer/batch/purchase_adjust'; COMMENT ON COLUMN tb_package_silence_extension_batch.approval_required IS '是否需要审批(按政策快照与类型判定,在线付费恒为 false)'; COMMENT ON COLUMN tb_package_silence_extension_batch.evidence_refs IS '客户申请记录、截图等附件对象存储 Key'; COMMENT ON COLUMN tb_package_silence_extension_batch.basis IS '免费或优惠依据'; COMMENT ON COLUMN tb_package_silence_extension_batch.adjust_package_usage_id IS '购买时调整:对应套餐记录'; COMMENT ON COLUMN tb_package_silence_extension_batch.adjust_default_days IS '购买时调整:套餐默认沉默期'; COMMENT ON COLUMN tb_package_silence_extension_batch.adjust_requested_days IS '购买时调整:申请天数'; COMMENT ON COLUMN tb_package_silence_extension_batch.amount IS '批次费用合计(分,免费为 0)'; COMMENT ON COLUMN tb_package_silence_extension_batch.offline_trade_no IS '优惠价有收款时的线下交易单号'; COMMENT ON COLUMN tb_package_silence_extension_batch.approval_status IS '审批状态:pending/approved/rejected/canceled'; COMMENT ON COLUMN tb_package_silence_extension_batch.status IS '批次状态:pending_payment/pending_approval/executing/completed/partially_completed/rejected/closed/failed'; CREATE TABLE tb_package_silence_extension ( id BIGSERIAL PRIMARY KEY, batch_id BIGINT NOT NULL, row_no INTEGER NOT NULL DEFAULT 1, package_usage_id BIGINT NOT NULL, package_id BIGINT NOT NULL DEFAULT 0, package_name_snapshot VARCHAR(255) NOT NULL DEFAULT '', asset_type VARCHAR(20) NOT NULL DEFAULT '', asset_id BIGINT NOT NULL DEFAULT 0, asset_identifier_snapshot VARCHAR(100) NOT NULL DEFAULT '', extension_type VARCHAR(30) NOT NULL, input_days INTEGER NOT NULL, months INTEGER NOT NULL, extension_days INTEGER NOT NULL, unit_price_snapshot BIGINT NOT NULL DEFAULT 0, amount BIGINT NOT NULL DEFAULT 0, extension_order_id BIGINT, before_deadline_at TIMESTAMPTZ, after_deadline_at TIMESTAMPTZ, status VARCHAR(20) NOT NULL, failure_code VARCHAR(64) NOT NULL DEFAULT '', failure_summary VARCHAR(500) NOT NULL DEFAULT '', executed_at TIMESTAMPTZ, handled_by BIGINT NOT NULL DEFAULT 0, handled_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT ck_silence_extension_status CHECK (status IN ( 'pending_payment', 'pending_approval', 'approved', 'executing', 'completed', 'exception', 'failed', 'rejected', 'closed' )), CONSTRAINT ck_silence_extension_days CHECK (input_days >= 1 AND months >= 1 AND extension_days = months * 30) ); CREATE UNIQUE INDEX uq_silence_extension_batch_usage ON tb_package_silence_extension (batch_id, package_usage_id); CREATE UNIQUE INDEX uq_silence_extension_order ON tb_package_silence_extension (extension_order_id) WHERE extension_order_id IS NOT NULL; CREATE UNIQUE INDEX uq_silence_extension_usage_open ON tb_package_silence_extension (package_usage_id) WHERE status IN ('pending_payment', 'pending_approval', 'approved', 'executing', 'exception'); CREATE INDEX idx_silence_extension_usage ON tb_package_silence_extension (package_usage_id, id); CREATE INDEX idx_silence_extension_status ON tb_package_silence_extension (status, updated_at DESC); COMMENT ON TABLE tb_package_silence_extension IS '沉默期延期明细(延期事实):每笔套餐一条,同一套餐同时只有一条未结束明细'; COMMENT ON COLUMN tb_package_silence_extension.row_no IS '批量导入时的明细行号'; COMMENT ON COLUMN tb_package_silence_extension.input_days IS '输入天数'; COMMENT ON COLUMN tb_package_silence_extension.months IS '计费月数 = ceil(输入天数 / 30)'; COMMENT ON COLUMN tb_package_silence_extension.extension_days IS '计费 / 延长天数 = 月数 × 30'; COMMENT ON COLUMN tb_package_silence_extension.extension_order_id IS '付费延期对应的延期订单(特殊延期为空)'; COMMENT ON COLUMN tb_package_silence_extension.status IS '明细状态:pending_payment/pending_approval/approved/executing/completed/exception/failed/rejected/closed'; COMMENT ON COLUMN tb_package_silence_extension.failure_code IS '失败或异常原因码'; CREATE TABLE tb_package_silence_extension_order ( id BIGSERIAL PRIMARY KEY, order_no VARCHAR(40) NOT NULL, batch_id BIGINT NOT NULL, package_usage_id BIGINT NOT NULL, package_id BIGINT NOT NULL, package_name_snapshot VARCHAR(255) NOT NULL DEFAULT '', asset_type VARCHAR(20) NOT NULL, asset_id BIGINT NOT NULL, asset_identifier_snapshot VARCHAR(100) NOT NULL DEFAULT '', shop_id BIGINT NOT NULL DEFAULT 0, customer_id BIGINT, source VARCHAR(20) NOT NULL, created_by BIGINT, evidence_refs JSONB NOT NULL DEFAULT '[]', input_days INTEGER NOT NULL, months INTEGER NOT NULL, extension_days INTEGER NOT NULL, policy_id BIGINT, policy_name_snapshot VARCHAR(100) NOT NULL DEFAULT '', unit_price BIGINT NOT NULL, approval_exempt_types_snapshot JSONB NOT NULL DEFAULT '[]', free_extension_limit_snapshot INTEGER NOT NULL DEFAULT 1, paid_max_months_snapshot INTEGER NOT NULL DEFAULT 0, single_max_months_snapshot INTEGER, amount BIGINT NOT NULL, before_deadline_at TIMESTAMPTZ NOT NULL, expected_deadline_at TIMESTAMPTZ NOT NULL, status VARCHAR(20) NOT NULL, payment_method VARCHAR(20) NOT NULL, offline_payment_method_id BIGINT, offline_payment_method_code VARCHAR(64), offline_payment_method_name VARCHAR(100), offline_trade_no VARCHAR(128), payment_voucher_keys JSONB NOT NULL DEFAULT '[]', approval_required BOOLEAN NOT NULL DEFAULT FALSE, approval_instance_id BIGINT, paid_payment_id BIGINT, paid_amount BIGINT, paid_at TIMESTAMPTZ, expire_at TIMESTAMPTZ, extension_id BIGINT, completed_at TIMESTAMPTZ, closed_at TIMESTAMPTZ, close_reason VARCHAR(200) NOT NULL DEFAULT '', exception_code VARCHAR(64) NOT NULL DEFAULT '', exception_summary VARCHAR(500) NOT NULL DEFAULT '', refund_no VARCHAR(64), refund_amount BIGINT, refunded_at TIMESTAMPTZ, refund_reason VARCHAR(500), request_id VARCHAR(64), shop_id_tag BIGINT NOT NULL DEFAULT 0, enterprise_id_tag BIGINT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT ck_silence_order_source CHECK (source IN ('client', 'admin', 'offline')), CONSTRAINT ck_silence_order_status CHECK (status IN ( 'pending_payment', 'pending_approval', 'rejected', 'paid', 'completed', 'closed', 'exception', 'refunded' )), CONSTRAINT ck_silence_order_payment_method CHECK (payment_method IN ('wechat', 'alipay', 'offline')), CONSTRAINT ck_silence_order_amount CHECK (amount >= 0 AND unit_price >= 0), CONSTRAINT ck_silence_order_days CHECK (input_days >= 1 AND months >= 1 AND extension_days = months * 30) ); CREATE UNIQUE INDEX uq_silence_order_no ON tb_package_silence_extension_order (order_no); CREATE UNIQUE INDEX uq_silence_order_usage_open ON tb_package_silence_extension_order (package_usage_id) WHERE status IN ('pending_payment', 'pending_approval'); CREATE UNIQUE INDEX uq_silence_order_offline_trade ON tb_package_silence_extension_order (offline_payment_method_id, offline_trade_no) WHERE offline_trade_no IS NOT NULL; CREATE UNIQUE INDEX uq_silence_order_request ON tb_package_silence_extension_order (request_id) WHERE request_id IS NOT NULL; CREATE INDEX idx_silence_order_usage ON tb_package_silence_extension_order (package_usage_id, id); CREATE INDEX idx_silence_order_status ON tb_package_silence_extension_order (status, created_at DESC); CREATE INDEX idx_silence_order_customer ON tb_package_silence_extension_order (customer_id, created_at DESC) WHERE customer_id IS NOT NULL; CREATE INDEX idx_silence_order_shop ON tb_package_silence_extension_order (shop_id_tag, created_at DESC); CREATE INDEX idx_silence_order_asset ON tb_package_silence_extension_order (asset_type, asset_id); CREATE INDEX idx_silence_order_pending_expire ON tb_package_silence_extension_order (expire_at) WHERE status = 'pending_payment'; COMMENT ON TABLE tb_package_silence_extension_order IS '沉默期延期订单(交易记录):在线支付复用 tb_payment(order_type = silence_extension),交易记录不删除'; COMMENT ON COLUMN tb_package_silence_extension_order.source IS '来源:client-客户 H5 下单 admin-运营后台生成二维码 offline-运营登记线下付款'; COMMENT ON COLUMN tb_package_silence_extension_order.input_days IS '客户输入天数'; COMMENT ON COLUMN tb_package_silence_extension_order.months IS '计费月数 = ceil(输入天数 / 30)'; COMMENT ON COLUMN tb_package_silence_extension_order.extension_days IS '计费 / 延长天数 = 月数 × 30'; COMMENT ON COLUMN tb_package_silence_extension_order.unit_price IS '单价快照(分/月,取下单时匹配的政策)'; COMMENT ON COLUMN tb_package_silence_extension_order.amount IS '应付金额快照(分),回调不重新计价'; COMMENT ON COLUMN tb_package_silence_extension_order.before_deadline_at IS '下单时截止时间'; COMMENT ON COLUMN tb_package_silence_extension_order.expected_deadline_at IS '预计新截止时间'; COMMENT ON COLUMN tb_package_silence_extension_order.status IS '状态:pending_payment/pending_approval/rejected/paid/completed/closed/exception/refunded'; COMMENT ON COLUMN tb_package_silence_extension_order.payment_method IS '支付方式:wechat/alipay(在线)/ offline(线下)'; COMMENT ON COLUMN tb_package_silence_extension_order.offline_trade_no IS '线下交易单号(与收款方式联合唯一)'; COMMENT ON COLUMN tb_package_silence_extension_order.paid_payment_id IS '最终支付成功的 tb_payment(商户快照、交易流水号从这张支付单取)'; COMMENT ON COLUMN tb_package_silence_extension_order.expire_at IS '支付截止时间,超时自动关闭'; COMMENT ON COLUMN tb_package_silence_extension_order.refund_no IS '仅执行异常原路退款时有值(延期费正常不退)'; COMMENT ON COLUMN tb_package_silence_extension_order.request_id IS '下单幂等键';