feat(server): 工程骨架后端——上传 → 解析 → 清单最小链路
- 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>
This commit is contained in:
110
server/migrations/sql/0001_baseline/02_billing.sql
Normal file
110
server/migrations/sql/0001_baseline/02_billing.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- 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)
|
||||
);
|
||||
Reference in New Issue
Block a user