- core:配置(prod 拒绝默认密钥 / dev 短信 / 假模型)、租户事务 + 行级安全策略、 problem+json 错误码、信封加密与签名下载、对象存储、事务内入队、Valkey stream 事件流、权限点 - identity:手机号 HMAC 存储、注册、登录(挤下线需确认,API3)、服务端会话 - billing:额度流水只追加,预扣 / 结算 / 退回,一笔预扣只有一个结局 - document:分片上传、按文件头识别格式、Gotenberg 转换、文字层与稳定句子编号 - checklist:标注规则识别、模型抽取、三道校验、标注类条款规则兜底、重跑对齐 - ai:提示词注册表(extract.substantive v1 来自 S4 v2)、假模型、OpenAI 兼容供应商、ai_call 流水 - 迁移:data-model 基线、套餐、Procrastinate、会话状态函数 - 测试 30 个:租户隔离、额度、失败退回、对象加密、契约一致性 - Compose(含 migrate)、镜像、CI Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
5.8 KiB
SQL
111 lines
5.8 KiB
SQL
-- 2. billing:套餐、订阅、开通申请、额度(架构 §3.9,FR-M6-03、05)
|
||
-- 来源:docs/architecture/schema-draft.sql(2026-11-03 验证过的草案)
|
||
-- 全局表:套餐定义带版本,D2 定价之后改数字就是加一个版本
|
||
CREATE TABLE plan (
|
||
code text NOT NULL CHECK (code IN ('free', 'pro', 'enterprise')),
|
||
version int NOT NULL,
|
||
name text NOT NULL,
|
||
quotas jsonb NOT NULL, -- {"parse": 500, "generate": 300};检查怎么扣见 data-model §4.3
|
||
limits jsonb NOT NULL, -- 单文件大小、页数、项目数上限
|
||
active boolean NOT NULL DEFAULT true,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
PRIMARY KEY (code, version)
|
||
);
|
||
|
||
CREATE TABLE activation_request ( -- 专业版开通申请(G8);注销后按财务要求保留
|
||
id uuid PRIMARY KEY,
|
||
tenant_id uuid NOT NULL REFERENCES tenant (id),
|
||
applicant_user_id uuid NOT NULL REFERENCES user_account (id),
|
||
plan_code text NOT NULL,
|
||
plan_version int NOT NULL,
|
||
invoice_info jsonb NOT NULL, -- 抬头、税号、地址电话、开户行账号
|
||
status text NOT NULL DEFAULT 'submitted'
|
||
CHECK (status IN ('submitted', 'paid', 'activated', 'rejected', 'cancelled')),
|
||
ops_note text,
|
||
handled_by_ops uuid,
|
||
handled_at timestamptz,
|
||
invoice_no text,
|
||
invoiced_at timestamptz,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, id),
|
||
FOREIGN KEY (plan_code, plan_version) REFERENCES plan (code, version)
|
||
);
|
||
|
||
CREATE TABLE subscription (
|
||
id uuid PRIMARY KEY,
|
||
tenant_id uuid NOT NULL REFERENCES tenant (id),
|
||
plan_code text NOT NULL,
|
||
plan_version int NOT NULL,
|
||
status text NOT NULL DEFAULT 'active'
|
||
CHECK (status IN ('active', 'expired', 'cancelled')),
|
||
source text NOT NULL CHECK (source IN ('signup', 'manual', 'online')),
|
||
seats int NOT NULL DEFAULT 1 CHECK (seats >= 1), -- MVP 固定 1(AR11)
|
||
starts_at timestamptz NOT NULL,
|
||
ends_at timestamptz, -- 免费版为空
|
||
activation_request_id uuid,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, id),
|
||
FOREIGN KEY (plan_code, plan_version) REFERENCES plan (code, version),
|
||
FOREIGN KEY (tenant_id, activation_request_id) REFERENCES activation_request (tenant_id, id)
|
||
);
|
||
-- 同一时间只有一个生效的订阅
|
||
CREATE UNIQUE INDEX ON subscription (tenant_id) WHERE status = 'active';
|
||
|
||
CREATE TABLE quota_account (
|
||
id uuid PRIMARY KEY,
|
||
tenant_id uuid NOT NULL REFERENCES tenant (id),
|
||
unit text NOT NULL CHECK (unit IN ('parse', 'check', 'generate', 'dedupe')),
|
||
balance int NOT NULL DEFAULT 0, -- 缓存,和流水在同一个事务里更新,定期对账
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, id),
|
||
UNIQUE (tenant_id, unit)
|
||
);
|
||
|
||
-- 只追加。amount 是对余额的影响:发放 +N、预扣 −1、退回 +1、结算 0、免扣 0、过期 −余量、调整 ±N
|
||
CREATE TABLE quota_entry (
|
||
id uuid PRIMARY KEY,
|
||
tenant_id uuid NOT NULL,
|
||
account_id uuid NOT NULL,
|
||
subscription_id uuid, -- 发放和过期挂在订阅周期上;「本期已用」按它汇总
|
||
kind text NOT NULL
|
||
CHECK (kind IN ('grant', 'hold', 'settle', 'release', 'expire', 'adjust', 'waive')),
|
||
amount int NOT NULL,
|
||
idempotency_key text NOT NULL, -- 如 parse_job:<id>:hold
|
||
hold_entry_id uuid, -- 结算、退回指回对应的预扣
|
||
ref_type text CHECK (ref_type IN ('parse_job', 'check_run')),
|
||
ref_id uuid, -- 不加外键:删除项目后流水保留(G10)
|
||
project_id uuid, -- 同上
|
||
reason text, -- 调整必填;免扣写规则,如 recheck_24h
|
||
actor_user_id uuid REFERENCES user_account (id),
|
||
actor_ops_id uuid,
|
||
created_at timestamptz NOT NULL DEFAULT now(),
|
||
UNIQUE (tenant_id, id),
|
||
UNIQUE (tenant_id, idempotency_key),
|
||
FOREIGN KEY (tenant_id, account_id) REFERENCES quota_account (tenant_id, id),
|
||
FOREIGN KEY (tenant_id, subscription_id) REFERENCES subscription (tenant_id, id),
|
||
FOREIGN KEY (tenant_id, hold_entry_id) REFERENCES quota_entry (tenant_id, id),
|
||
CHECK (kind <> 'adjust' OR reason IS NOT NULL),
|
||
CHECK (kind NOT IN ('settle', 'waive') OR amount = 0),
|
||
CHECK (kind <> 'hold' OR amount < 0),
|
||
CHECK (kind NOT IN ('settle', 'release') OR hold_entry_id IS NOT NULL)
|
||
);
|
||
CREATE INDEX ON quota_entry (tenant_id, account_id, created_at DESC);
|
||
CREATE INDEX ON quota_entry (tenant_id, ref_type, ref_id);
|
||
-- 一笔预扣只能有一个结局
|
||
CREATE UNIQUE INDEX ON quota_entry (tenant_id, hold_entry_id) WHERE kind IN ('settle', 'release');
|
||
CREATE TRIGGER quota_entry_append_only BEFORE UPDATE OR DELETE ON quota_entry
|
||
FOR EACH ROW EXECUTE FUNCTION forbid_change();
|
||
|
||
CREATE TABLE quota_alert ( -- 80% / 100% 每个周期只提醒一次
|
||
tenant_id uuid NOT NULL,
|
||
account_id uuid NOT NULL,
|
||
subscription_id uuid NOT NULL,
|
||
threshold int NOT NULL CHECK (threshold IN (80, 100)),
|
||
sent_at timestamptz NOT NULL DEFAULT now(),
|
||
PRIMARY KEY (tenant_id, account_id, subscription_id, threshold),
|
||
FOREIGN KEY (tenant_id, account_id) REFERENCES quota_account (tenant_id, id),
|
||
FOREIGN KEY (tenant_id, subscription_id) REFERENCES subscription (tenant_id, id)
|
||
);
|