reset backend

This commit is contained in:
2025-12-27 19:49:11 +08:00
parent 77ac9d00b3
commit 503b15aab7
129 changed files with 1134 additions and 18084 deletions

View File

@@ -1,29 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS users(
id bigserial PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
deleted_at timestamptz,
username varchar(255) NOT NULL UNIQUE,
password varchar(255) NOT NULL,
roles text[] NOT NULL DEFAULT ARRAY['user'],
status varchar(50) NOT NULL DEFAULT 'active',
metas jsonb NOT NULL DEFAULT '{}',
balance bigint NOT NULL DEFAULT 0,
balance_frozen bigint NOT NULL DEFAULT 0,
verified_at timestamptz
);
COMMENT ON COLUMN users.balance IS '全局可用余额:分/最小货币单位;用户在所有已加入租户内共享该余额;默认 0';
COMMENT ON COLUMN users.balance_frozen IS '全局冻结余额:分/最小货币单位;用于下单冻结等;默认 0';
CREATE INDEX IF NOT EXISTS ix_users_balance ON users(balance);
CREATE INDEX IF NOT EXISTS ix_users_balance_frozen ON users(balance_frozen);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS users;
-- +goose StatementEnd

View File

@@ -1,29 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS tenants(
id bigserial PRIMARY KEY,
user_id bigint NOT NULL,
code varchar(64) NOT NULL,
uuid uuid NOT NULL,
name varchar(128) NOT NULL DEFAULT '',
status varchar(64) NOT NULL DEFAULT '',
config jsonb NOT NULL DEFAULT '{}'::jsonb,
expired_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_code_lower ON tenants(lower(code));
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenants_uuid ON tenants(uuid);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ux_tenants_uuid;
DROP INDEX IF EXISTS ux_tenants_code_lower;
DROP TABLE IF EXISTS tenants;
-- +goose StatementEnd

View File

@@ -1,19 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS tenant_users(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
role TEXT[] NOT NULL DEFAULT ARRAY['member'],
status varchar(50) NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, user_id)
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS tenant_users;
-- +goose StatementEnd

View File

@@ -1,190 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS media_assets(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
type varchar(32) NOT NULL DEFAULT 'video',
status varchar(32) NOT NULL DEFAULT 'uploaded',
provider varchar(64) NOT NULL DEFAULT '',
bucket varchar(128) NOT NULL DEFAULT '',
object_key varchar(512) NOT NULL DEFAULT '',
meta jsonb NOT NULL DEFAULT '{}'::jsonb,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- media_assets媒体资源表视频/音频/图片),用于承载实际文件对象及其处理状态
COMMENT ON TABLE media_assets IS '媒体资源存储对象的抽象video/audio/image关联租户与上传用户记录处理状态与元数据';
COMMENT ON COLUMN media_assets.id IS '主键ID自增仅用于内部关联';
COMMENT ON COLUMN media_assets.tenant_id IS '租户ID多租户隔离关键字段所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN media_assets.user_id IS '用户ID资源上传者用于审计与权限控制';
COMMENT ON COLUMN media_assets.type IS '资源类型video/audio/image决定后续处理流程转码/缩略图/封面等)';
COMMENT ON COLUMN media_assets.status IS '处理状态uploaded/processing/ready/failed/deletedready 才可被内容引用对外提供';
COMMENT ON COLUMN media_assets.provider IS '存储提供方:例如 s3/minio/oss便于多存储扩展';
COMMENT ON COLUMN media_assets.bucket IS '存储桶:对象所在 bucket与 provider 组合确定存储定位';
COMMENT ON COLUMN media_assets.object_key IS '对象键:对象在 bucket 内的 key不得暴露可长期复用的直链通过签名URL/token下发';
COMMENT ON COLUMN media_assets.meta IS '元数据JSON包含 hash、duration、width、height、bitrate、codec 等;用于展示与计费/风控';
COMMENT ON COLUMN media_assets.deleted_at IS '软删除时间:非空表示已删除;对外接口需过滤';
COMMENT ON COLUMN media_assets.created_at IS '创建时间:默认 now();用于审计与排序';
COMMENT ON COLUMN media_assets.updated_at IS '更新时间:默认 now();更新状态/元数据时写入';
CREATE INDEX IF NOT EXISTS ix_media_assets_tenant_id ON media_assets(tenant_id);
CREATE INDEX IF NOT EXISTS ix_media_assets_tenant_user_id ON media_assets(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_media_assets_tenant_status ON media_assets(tenant_id, status);
CREATE TABLE IF NOT EXISTS contents(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
title varchar(255) NOT NULL DEFAULT '',
description text NOT NULL DEFAULT '',
status varchar(32) NOT NULL DEFAULT 'draft',
visibility varchar(32) NOT NULL DEFAULT 'tenant_only',
preview_seconds int NOT NULL DEFAULT 60,
preview_downloadable boolean NOT NULL DEFAULT false,
published_at timestamptz,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- contents内容表可发布/可售卖/可试看)
COMMENT ON TABLE contents IS '内容:可发布的媒体内容实体,承载标题描述、可见性、试看配置、发布状态等';
COMMENT ON COLUMN contents.id IS '主键ID自增用于内容引用';
COMMENT ON COLUMN contents.tenant_id IS '租户ID多租户隔离关键字段所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN contents.user_id IS '用户ID内容创建者/发布者;用于权限与审计(例如私有内容仅作者可见)';
COMMENT ON COLUMN contents.title IS '标题:用于列表展示与搜索;建议限制长度(由业务校验)';
COMMENT ON COLUMN contents.description IS '描述:用于详情页展示;可为空字符串';
COMMENT ON COLUMN contents.status IS '状态draft/reviewing/published/unpublished/blockedpublished 才对外展示';
COMMENT ON COLUMN contents.visibility IS '可见性public/tenant_only/private仅控制详情可见正片资源仍需按价格/权益校验';
COMMENT ON COLUMN contents.preview_seconds IS '试看秒数:默认 60只对 preview 资源生效;必须为正整数';
COMMENT ON COLUMN contents.preview_downloadable IS '试看是否允许下载:默认 false当前策略固定为不允许下载仅 streaming';
COMMENT ON COLUMN contents.published_at IS '发布时间:首次发布时写入;用于时间窗与排序';
COMMENT ON COLUMN contents.deleted_at IS '软删除时间:非空表示已删除;对外接口需过滤';
COMMENT ON COLUMN contents.created_at IS '创建时间:默认 now();用于审计与排序';
COMMENT ON COLUMN contents.updated_at IS '更新时间:默认 now();编辑内容时写入';
CREATE INDEX IF NOT EXISTS ix_contents_tenant_id ON contents(tenant_id);
CREATE INDEX IF NOT EXISTS ix_contents_tenant_user_id ON contents(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_contents_tenant_status ON contents(tenant_id, status);
CREATE INDEX IF NOT EXISTS ix_contents_tenant_visibility ON contents(tenant_id, visibility);
CREATE TABLE IF NOT EXISTS content_assets(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
content_id bigint NOT NULL,
asset_id bigint NOT NULL,
role varchar(32) NOT NULL DEFAULT 'main',
sort int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, content_id, asset_id)
);
-- content_assets内容与媒体资源的关联区分 main/cover/preview
COMMENT ON TABLE content_assets IS '内容-资源关联:将 media_assets 以角色main/cover/preview绑定到 contents支持排序';
COMMENT ON COLUMN content_assets.id IS '主键ID自增';
COMMENT ON COLUMN content_assets.tenant_id IS '租户ID多租户隔离必须与 content_id、asset_id 所属租户一致';
COMMENT ON COLUMN content_assets.user_id IS '用户ID操作人/绑定人;用于审计(通常为租户管理员或作者)';
COMMENT ON COLUMN content_assets.content_id IS '内容ID关联 contents.id用于查询内容下资源列表';
COMMENT ON COLUMN content_assets.asset_id IS '资源ID关联 media_assets.id用于查询资源归属内容';
COMMENT ON COLUMN content_assets.role IS '资源角色main/cover/previewpreview 必须为独立资源以满足禁下载与防绕过';
COMMENT ON COLUMN content_assets.sort IS '排序:同一 role 下的展示顺序,数值越小越靠前';
COMMENT ON COLUMN content_assets.created_at IS '创建时间:默认 now();用于审计';
COMMENT ON COLUMN content_assets.updated_at IS '更新时间:默认 now();更新 sort/role 时写入';
CREATE INDEX IF NOT EXISTS ix_content_assets_tenant_content ON content_assets(tenant_id, content_id);
CREATE INDEX IF NOT EXISTS ix_content_assets_tenant_asset ON content_assets(tenant_id, asset_id);
CREATE INDEX IF NOT EXISTS ix_content_assets_tenant_role ON content_assets(tenant_id, content_id, role);
CREATE TABLE IF NOT EXISTS content_prices(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
content_id bigint NOT NULL,
currency varchar(16) NOT NULL DEFAULT 'CNY',
price_amount bigint NOT NULL DEFAULT 0,
discount_type varchar(16) NOT NULL DEFAULT 'none',
discount_value bigint NOT NULL DEFAULT 0,
discount_start_at timestamptz,
discount_end_at timestamptz,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, content_id)
);
-- content_prices内容定价与折扣仅 CNY 分)
COMMENT ON TABLE content_prices IS '内容定价:为内容设置价格与折扣(订单需记录成交快照,避免改价影响历史)';
COMMENT ON COLUMN content_prices.id IS '主键ID自增';
COMMENT ON COLUMN content_prices.tenant_id IS '租户ID多租户隔离与内容归属一致';
COMMENT ON COLUMN content_prices.user_id IS '用户ID设置/更新价格的操作人(通常为 tenant_admin用于审计';
COMMENT ON COLUMN content_prices.content_id IS '内容ID唯一约束 (tenant_id, content_id);一个内容在一个租户内仅一份定价';
COMMENT ON COLUMN content_prices.currency IS '币种:当前固定 CNY金额单位为分';
COMMENT ON COLUMN content_prices.price_amount IS '基础价格0 表示免费(可直接访问正片资源)';
COMMENT ON COLUMN content_prices.discount_type IS '折扣类型none/percent/amount仅影响下单时成交价需写入订单快照';
COMMENT ON COLUMN content_prices.discount_value IS '折扣值percent=0-100按业务校验amount=分none 时忽略';
COMMENT ON COLUMN content_prices.discount_start_at IS '折扣开始时间:可为空;为空表示立即生效(由业务逻辑解释)';
COMMENT ON COLUMN content_prices.discount_end_at IS '折扣结束时间:可为空;为空表示长期有效(由业务逻辑解释)';
COMMENT ON COLUMN content_prices.created_at IS '创建时间:默认 now();用于审计';
COMMENT ON COLUMN content_prices.updated_at IS '更新时间:默认 now();更新价格/折扣时写入';
CREATE INDEX IF NOT EXISTS ix_content_prices_tenant_id ON content_prices(tenant_id);
CREATE TABLE IF NOT EXISTS content_access(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
content_id bigint NOT NULL,
order_id bigint,
status varchar(16) NOT NULL DEFAULT 'active',
revoked_at timestamptz,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, user_id, content_id)
);
-- content_access购买权益/访问权限(退款后撤销)
COMMENT ON TABLE content_access IS '内容权益:记录用户在租户内对内容的访问资格;退款后应立即 revoked';
COMMENT ON COLUMN content_access.id IS '主键ID自增';
COMMENT ON COLUMN content_access.tenant_id IS '租户ID多租户隔离与内容、用户归属一致';
COMMENT ON COLUMN content_access.user_id IS '用户ID权益所属用户用于访问校验';
COMMENT ON COLUMN content_access.content_id IS '内容ID权益对应内容唯一约束 (tenant_id, user_id, content_id)';
COMMENT ON COLUMN content_access.order_id IS '订单ID产生该权益的订单可为空例如后台补发/迁移)';
COMMENT ON COLUMN content_access.status IS '权益状态active/revoked/expiredrevoked 表示立即失效(例如退款/违规)';
COMMENT ON COLUMN content_access.revoked_at IS '撤销时间:当 status=revoked 时写入;用于审计与追责';
COMMENT ON COLUMN content_access.created_at IS '创建时间:默认 now();用于审计';
COMMENT ON COLUMN content_access.updated_at IS '更新时间:默认 now();更新 status 时写入';
CREATE INDEX IF NOT EXISTS ix_content_access_tenant_user ON content_access(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_content_access_tenant_content ON content_access(tenant_id, content_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_content_access_tenant_content;
DROP INDEX IF EXISTS ix_content_access_tenant_user;
DROP TABLE IF EXISTS content_access;
DROP INDEX IF EXISTS ix_content_prices_tenant_id;
DROP TABLE IF EXISTS content_prices;
DROP INDEX IF EXISTS ix_content_assets_tenant_role;
DROP INDEX IF EXISTS ix_content_assets_tenant_asset;
DROP INDEX IF EXISTS ix_content_assets_tenant_content;
DROP TABLE IF EXISTS content_assets;
DROP INDEX IF EXISTS ix_contents_tenant_visibility;
DROP INDEX IF EXISTS ix_contents_tenant_status;
DROP INDEX IF EXISTS ix_contents_tenant_user_id;
DROP INDEX IF EXISTS ix_contents_tenant_id;
DROP TABLE IF EXISTS contents;
DROP INDEX IF EXISTS ix_media_assets_tenant_status;
DROP INDEX IF EXISTS ix_media_assets_tenant_user_id;
DROP INDEX IF EXISTS ix_media_assets_tenant_id;
DROP TABLE IF EXISTS media_assets;
-- +goose StatementEnd

View File

@@ -1,138 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS orders(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
type varchar(32) NOT NULL DEFAULT 'content_purchase',
status varchar(32) NOT NULL DEFAULT 'created',
currency varchar(16) NOT NULL DEFAULT 'CNY',
amount_original bigint NOT NULL DEFAULT 0,
amount_discount bigint NOT NULL DEFAULT 0,
amount_paid bigint NOT NULL DEFAULT 0,
snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
idempotency_key varchar(128) NOT NULL DEFAULT '',
paid_at timestamptz,
refunded_at timestamptz,
refund_forced boolean NOT NULL DEFAULT false,
refund_operator_user_id bigint,
refund_reason varchar(255) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- orders订单主表租户内购买等业务单据
COMMENT ON TABLE orders IS '订单:租户内的业务交易单据;记录成交金额快照、状态流转与退款信息;所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN orders.id IS '主键ID自增用于关联订单明细、账本流水、权益等';
COMMENT ON COLUMN orders.tenant_id IS '租户ID多租户隔离关键字段所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN orders.user_id IS '用户ID下单用户buyer余额扣款与权益归属以该 user_id 为准';
COMMENT ON COLUMN orders.type IS '订单类型content_purchase购买内容当前默认 content_purchase';
COMMENT ON COLUMN orders.status IS '订单状态created/paid/refunding/refunded/canceled/failed状态变更需与账本/权益保持一致';
COMMENT ON COLUMN orders.currency IS '币种:当前固定 CNY金额单位为分';
COMMENT ON COLUMN orders.amount_original IS '原价金额:分;未折扣前金额(用于展示与对账)';
COMMENT ON COLUMN orders.amount_discount IS '优惠金额amount_paid = amount_original - amount_discount下单时快照';
COMMENT ON COLUMN orders.amount_paid IS '实付金额:分;从租户内余额扣款的金额(下单时快照)';
COMMENT ON COLUMN orders.snapshot IS '订单快照JSON建议包含 content 标题/定价/折扣、请求来源等,避免改价影响历史展示';
COMMENT ON COLUMN orders.idempotency_key IS '幂等键:同一租户同一用户同一业务请求可用;用于防重复下单/重复扣款(建议由客户端生成)';
COMMENT ON COLUMN orders.paid_at IS '支付/扣款完成时间:余额支付在 debit_purchase 成功后写入';
COMMENT ON COLUMN orders.refunded_at IS '退款完成时间:退款落账成功后写入';
COMMENT ON COLUMN orders.refund_forced IS '是否强制退款true 表示租户管理侧绕过时间窗执行退款(需审计)';
COMMENT ON COLUMN orders.refund_operator_user_id IS '退款操作人用户ID租户管理员/系统;用于审计与追责';
COMMENT ON COLUMN orders.refund_reason IS '退款原因:后台/用户发起退款的原因说明;用于审计';
COMMENT ON COLUMN orders.created_at IS '创建时间:默认 now();用于审计与排序';
COMMENT ON COLUMN orders.updated_at IS '更新时间:默认 now();状态变更/退款写入时更新';
CREATE INDEX IF NOT EXISTS ix_orders_tenant_user ON orders(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_orders_tenant_status ON orders(tenant_id, status);
CREATE INDEX IF NOT EXISTS ix_orders_tenant_paid_at ON orders(tenant_id, paid_at);
CREATE UNIQUE INDEX IF NOT EXISTS ux_orders_tenant_idempotency_key ON orders(tenant_id, user_id, idempotency_key) WHERE idempotency_key <> '';
CREATE TABLE IF NOT EXISTS order_items(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
order_id bigint NOT NULL,
content_id bigint NOT NULL,
content_user_id bigint NOT NULL DEFAULT 0,
amount_paid bigint NOT NULL DEFAULT 0,
snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, order_id, content_id)
);
-- order_items订单明细购买内容通常一单一内容但保留扩展能力
COMMENT ON TABLE order_items IS '订单明细:记录订单购买的具体内容及金额/快照;支持后续扩展为一单多内容';
COMMENT ON COLUMN order_items.id IS '主键ID自增';
COMMENT ON COLUMN order_items.tenant_id IS '租户ID多租户隔离关键字段必须与 orders.tenant_id 一致';
COMMENT ON COLUMN order_items.user_id IS '用户ID下单用户buyer冗余字段用于查询加速与审计';
COMMENT ON COLUMN order_items.order_id IS '订单ID关联 orders.id用于聚合订单明细';
COMMENT ON COLUMN order_items.content_id IS '内容ID关联 contents.id用于生成/撤销 content_access';
COMMENT ON COLUMN order_items.content_user_id IS '内容作者用户ID用于后续分成/对账扩展;当前可为 0 或写入内容创建者';
COMMENT ON COLUMN order_items.amount_paid IS '该行实付金额:分;通常等于订单 amount_paid单内容场景';
COMMENT ON COLUMN order_items.snapshot IS '内容快照JSON建议包含 title/price/discount 等,用于历史展示与审计';
COMMENT ON COLUMN order_items.created_at IS '创建时间:默认 now()';
COMMENT ON COLUMN order_items.updated_at IS '更新时间:默认 now()';
CREATE INDEX IF NOT EXISTS ix_order_items_tenant_order ON order_items(tenant_id, order_id);
CREATE INDEX IF NOT EXISTS ix_order_items_tenant_content ON order_items(tenant_id, content_id);
CREATE TABLE IF NOT EXISTS tenant_ledgers(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
order_id bigint,
type varchar(32) NOT NULL,
amount bigint NOT NULL DEFAULT 0,
balance_before bigint NOT NULL DEFAULT 0,
balance_after bigint NOT NULL DEFAULT 0,
frozen_before bigint NOT NULL DEFAULT 0,
frozen_after bigint NOT NULL DEFAULT 0,
idempotency_key varchar(128) NOT NULL DEFAULT '',
remark varchar(255) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- tenant_ledgers租户内余额账本流水必须可审计、可幂等
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.id IS '主键ID自增';
COMMENT ON COLUMN tenant_ledgers.tenant_id IS '租户ID多租户隔离关键字段必须与 tenant_users.tenant_id 一致';
COMMENT ON COLUMN tenant_ledgers.user_id IS '用户ID余额账户归属用户对应 tenant_users.user_id';
COMMENT ON COLUMN tenant_ledgers.order_id IS '关联订单ID购买/退款类流水应关联 orders.id非订单类可为空';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型debit_purchase/credit_refund/freeze/unfreeze/adjustment不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.amount IS '流水金额:分/最小货币单位;通常为正数,方向由 type 决定(由业务层约束)';
COMMENT ON COLUMN tenant_ledgers.balance_before IS '变更前可用余额:用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.balance_after IS '变更后可用余额:用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.frozen_before IS '变更前冻结余额:用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.frozen_after IS '变更后冻结余额:用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.idempotency_key IS '幂等键:同一租户同一用户同一业务操作固定;用于防止重复落账(建议由业务层生成)';
COMMENT ON COLUMN tenant_ledgers.remark IS '备注:业务说明/后台操作原因等;用于审计';
COMMENT ON COLUMN tenant_ledgers.created_at IS '创建时间:默认 now()';
COMMENT ON COLUMN tenant_ledgers.updated_at IS '更新时间:默认 now()';
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_user ON tenant_ledgers(tenant_id, user_id);
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_order ON tenant_ledgers(tenant_id, order_id);
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_type ON tenant_ledgers(tenant_id, type);
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_idempotency_key ON tenant_ledgers(tenant_id, user_id, idempotency_key) WHERE idempotency_key <> '';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_idempotency_key;
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_type;
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_order;
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_user;
DROP TABLE IF EXISTS tenant_ledgers;
DROP INDEX IF EXISTS ix_order_items_tenant_content;
DROP INDEX IF EXISTS ix_order_items_tenant_order;
DROP TABLE IF EXISTS order_items;
DROP INDEX IF EXISTS ux_orders_tenant_idempotency_key;
DROP INDEX IF EXISTS ix_orders_tenant_paid_at;
DROP INDEX IF EXISTS ix_orders_tenant_status;
DROP INDEX IF EXISTS ix_orders_tenant_user;
DROP TABLE IF EXISTS orders;
-- +goose StatementEnd

View File

@@ -1,11 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- orders.amount_paid为“按金额区间筛选订单”提供索引租户内隔离维度
CREATE INDEX IF NOT EXISTS ix_orders_tenant_amount_paid ON orders(tenant_id, amount_paid);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_orders_tenant_amount_paid;
-- +goose StatementEnd

View File

@@ -1,19 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- tenant_users.status历史上默认值为 'active',但代码枚举使用 UserStatuspending_verify/verified/banned
-- 为避免新增成员落入未知状态,这里将默认值调整为 'verified',并修正存量 'active' -> 'verified'。
ALTER TABLE tenant_users
ALTER COLUMN status SET DEFAULT 'verified';
UPDATE tenant_users
SET status = 'verified'
WHERE status = 'active';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- 回滚:恢复默认值为 'active'(不回滚数据修正)。
ALTER TABLE tenant_users
ALTER COLUMN status SET DEFAULT 'active';
-- +goose StatementEnd

View File

@@ -1,81 +0,0 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS tenant_invites(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
code varchar(64) NOT NULL,
status varchar(32) NOT NULL DEFAULT 'active',
max_uses int NOT NULL DEFAULT 0,
used_count int NOT NULL DEFAULT 0,
expires_at timestamptz,
disabled_at timestamptz,
disabled_operator_user_id bigint,
remark varchar(255) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW(),
UNIQUE (tenant_id, code)
);
-- tenant_invites租户邀请用于用户通过邀请码加入租户
COMMENT ON TABLE tenant_invites IS '租户邀请:租户管理员生成的邀请码;用户可通过 code 加入租户;支持禁用、过期、使用次数限制;所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN tenant_invites.id IS '主键ID自增';
COMMENT ON COLUMN tenant_invites.tenant_id IS '租户ID多租户隔离关键字段所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN tenant_invites.user_id IS '创建人用户ID生成邀请码的租户管理员审计用';
COMMENT ON COLUMN tenant_invites.code IS '邀请码:用户加入租户时提交;同一租户内唯一';
COMMENT ON COLUMN tenant_invites.status IS '邀请状态active/disabled/expiredexpired 也可由 expires_at 推导,业务侧需保持一致';
COMMENT ON COLUMN tenant_invites.max_uses IS '最大可使用次数0 表示不限制;>0 时 used_count 达到该值后视为失效';
COMMENT ON COLUMN tenant_invites.used_count IS '已使用次数:每次成功加入时 +1需事务保证并发下不超发';
COMMENT ON COLUMN tenant_invites.expires_at IS '过期时间到期后不可再使用UTC为空表示不过期';
COMMENT ON COLUMN tenant_invites.disabled_at IS '禁用时间租户管理员禁用该邀请的时间UTC';
COMMENT ON COLUMN tenant_invites.disabled_operator_user_id IS '禁用操作人用户ID租户管理员审计用';
COMMENT ON COLUMN tenant_invites.remark IS '备注:生成/禁用原因等(审计用)';
COMMENT ON COLUMN tenant_invites.created_at IS '创建时间:默认 now()';
COMMENT ON COLUMN tenant_invites.updated_at IS '更新时间:默认 now()';
CREATE INDEX IF NOT EXISTS ix_tenant_invites_tenant_status ON tenant_invites(tenant_id, status);
CREATE INDEX IF NOT EXISTS ix_tenant_invites_tenant_expires_at ON tenant_invites(tenant_id, expires_at);
CREATE TABLE IF NOT EXISTS tenant_join_requests(
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
user_id bigint NOT NULL,
status varchar(32) NOT NULL DEFAULT 'pending',
reason varchar(255) NOT NULL DEFAULT '',
decided_at timestamptz,
decided_operator_user_id bigint,
decided_reason varchar(255) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT NOW(),
updated_at timestamptz NOT NULL DEFAULT NOW()
);
-- tenant_join_requests加入租户申请用于无邀请码场景的人工审核
COMMENT ON TABLE tenant_join_requests IS '加入申请:用户申请加入租户,租户管理员审核通过/拒绝;所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN tenant_join_requests.id IS '主键ID自增';
COMMENT ON COLUMN tenant_join_requests.tenant_id IS '租户ID多租户隔离关键字段所有查询/写入必须限定 tenant_id';
COMMENT ON COLUMN tenant_join_requests.user_id IS '申请人用户ID发起加入申请的用户';
COMMENT ON COLUMN tenant_join_requests.status IS '申请状态pending/approved/rejected状态变更需记录 decided_at 与 decided_operator_user_id';
COMMENT ON COLUMN tenant_join_requests.reason IS '申请原因:用户填写的加入说明(可选)';
COMMENT ON COLUMN tenant_join_requests.decided_at IS '处理时间:审核通过/拒绝时记录UTC';
COMMENT ON COLUMN tenant_join_requests.decided_operator_user_id IS '处理人用户ID租户管理员审计用';
COMMENT ON COLUMN tenant_join_requests.decided_reason IS '处理说明:管理员通过/拒绝的原因(可选,审计用)';
COMMENT ON COLUMN tenant_join_requests.created_at IS '创建时间:默认 now()';
COMMENT ON COLUMN tenant_join_requests.updated_at IS '更新时间:默认 now()';
-- 约束:同一用户同一租户同一时间仅允许存在一个 pending 申请,避免重复提交淹没审核队列。
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_join_requests_tenant_user_pending ON tenant_join_requests(tenant_id, user_id) WHERE status = 'pending';
CREATE INDEX IF NOT EXISTS ix_tenant_join_requests_tenant_status ON tenant_join_requests(tenant_id, status);
CREATE INDEX IF NOT EXISTS ix_tenant_join_requests_tenant_created_at ON tenant_join_requests(tenant_id, created_at);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_tenant_join_requests_tenant_created_at;
DROP INDEX IF EXISTS ix_tenant_join_requests_tenant_status;
DROP INDEX IF EXISTS ux_tenant_join_requests_tenant_user_pending;
DROP TABLE IF EXISTS tenant_join_requests;
DROP INDEX IF EXISTS ix_tenant_invites_tenant_expires_at;
DROP INDEX IF EXISTS ix_tenant_invites_tenant_status;
DROP TABLE IF EXISTS tenant_invites;
-- +goose StatementEnd

View File

@@ -1,11 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- orders 列表查询索引补齐:租户管理端常用按 created_at/type 过滤 + 排序。
CREATE INDEX IF NOT EXISTS ix_orders_tenant_created_at ON orders(tenant_id, created_at);
CREATE INDEX IF NOT EXISTS ix_orders_tenant_type ON orders(tenant_id, type);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_orders_tenant_type;
DROP INDEX IF EXISTS ix_orders_tenant_created_at;
-- +goose StatementEnd

View File

@@ -1,36 +0,0 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE media_assets
ADD COLUMN IF NOT EXISTS variant varchar(32) NOT NULL DEFAULT 'main';
-- 回填历史数据:老数据一律视为 main。
UPDATE media_assets
SET variant = 'main'
WHERE variant IS NULL OR variant = '';
-- 约束:只允许 main/preview
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'ck_media_assets_variant'
) THEN
ALTER TABLE media_assets
ADD CONSTRAINT ck_media_assets_variant
CHECK (variant IN ('main', 'preview'));
END IF;
END
$$;
COMMENT ON COLUMN media_assets.variant IS '产物类型main/preview用于强制试看资源必须绑定独立产物避免用正片绕过';
CREATE INDEX IF NOT EXISTS ix_media_assets_tenant_variant ON media_assets (tenant_id, variant);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_media_assets_tenant_variant;
ALTER TABLE media_assets DROP CONSTRAINT IF EXISTS ck_media_assets_variant;
ALTER TABLE media_assets DROP COLUMN IF EXISTS variant;
-- +goose StatementEnd

View File

@@ -1,16 +0,0 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE media_assets
ADD COLUMN IF NOT EXISTS source_asset_id bigint;
COMMENT ON COLUMN media_assets.source_asset_id IS '派生来源资源IDpreview 产物可指向对应 main 资源;用于建立 preview/main 的 1:1 追溯关系';
CREATE INDEX IF NOT EXISTS ix_media_assets_tenant_source_asset_id ON media_assets (tenant_id, source_asset_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_media_assets_tenant_source_asset_id;
ALTER TABLE media_assets DROP COLUMN IF EXISTS source_asset_id;
-- +goose StatementEnd

View File

@@ -1,40 +0,0 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE tenant_ledgers
ADD COLUMN IF NOT EXISTS operator_user_id bigint,
ADD COLUMN IF NOT EXISTS biz_ref_type varchar(32),
ADD COLUMN IF NOT EXISTS biz_ref_id bigint;
-- tenant_ledgers.operator_user_id操作者谁触发该流水
-- 用途:用于审计与风控追溯(例如后台代退款/调账等)。
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID谁触发该流水admin/buyer/system用于审计与追责可为空历史数据或无法识别时';
-- tenant_ledgers.biz_ref_type/biz_ref_id业务引用幂等与追溯
-- 用途:在 idempotency_key 之外提供结构化引用(例如 order/refund 等),便于报表与按业务对象追溯。
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型order/refund/etc与 biz_ref_id 组成可选的结构化幂等/追溯键';
COMMENT ON COLUMN tenant_ledgers.biz_ref_id IS '业务引用ID与 biz_ref_type 配合使用(例如 orders.id用于对账与审计';
-- 索引:按操作者检索敏感操作流水(后台审计用)。
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_operator ON tenant_ledgers(tenant_id, operator_user_id);
-- 索引:按业务引用快速定位同一业务对象的流水集合。
CREATE INDEX IF NOT EXISTS ix_tenant_ledgers_tenant_biz_ref ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id);
-- 结构化幂等(可选):同一业务引用在同一流水类型下只能出现一条。
-- 说明biz_ref_* 允许为空;仅当两者都非空时才参与唯一性约束。
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
WHERE biz_ref_type IS NOT NULL AND biz_ref_id IS NOT NULL;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_biz_ref;
DROP INDEX IF EXISTS ix_tenant_ledgers_tenant_operator;
ALTER TABLE tenant_ledgers
DROP COLUMN IF EXISTS biz_ref_id,
DROP COLUMN IF EXISTS biz_ref_type,
DROP COLUMN IF EXISTS operator_user_id;
-- +goose StatementEnd

View File

@@ -1,22 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- 修正biz_ref_type/biz_ref_id 在 Go 模型侧为 string/int64非指针空值会写入 ''/0
-- 若唯一索引仅判断 NOT NULL会导致大量流水写入冲突。
-- 约束策略:仅当 biz_ref_type 非空 且 biz_ref_id > 0 时才参与唯一性约束。
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
WHERE biz_ref_type IS NOT NULL AND biz_ref_type <> '' AND biz_ref_id IS NOT NULL AND biz_ref_id <> 0;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type;
-- Down 回滚为“仅判断 NOT NULL”的版本不建议在线上使用该版本
CREATE UNIQUE INDEX IF NOT EXISTS ux_tenant_ledgers_tenant_biz_ref_type_id_type
ON tenant_ledgers(tenant_id, biz_ref_type, biz_ref_id, type)
WHERE biz_ref_type IS NOT NULL AND biz_ref_id IS NOT NULL;
-- +goose StatementEnd

View File

@@ -1,24 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- 清理“充值”遗留描述:当前项目已移除租户充值与 per-tenant 余额。
COMMENT ON COLUMN orders.type IS '订单类型content_purchase购买内容当前默认 content_purchase';
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型debit_purchase/credit_refund/freeze/unfreeze/adjustment不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID谁触发该流水admin/buyer/system用于审计与追责可为空历史数据或无法识别时';
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型order/refund/etc与 biz_ref_id 组成可选的结构化幂等/追溯键';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
-- 新项目不需要依赖 Down 做历史回滚;保持与 Up 一致,避免引入已移除特性的遗留描述。
COMMENT ON COLUMN orders.type IS '订单类型content_purchase购买内容当前默认 content_purchase';
COMMENT ON TABLE tenant_ledgers IS '账本流水:记录租户内用户余额的每一次变化(冻结/扣款/退款/调账等);用于审计与对账回放';
COMMENT ON COLUMN tenant_ledgers.type IS '流水类型debit_purchase/credit_refund/freeze/unfreeze/adjustment不同类型决定余额/冻结余额的变更方向';
COMMENT ON COLUMN tenant_ledgers.operator_user_id IS '操作者用户ID谁触发该流水admin/buyer/system用于审计与追责可为空历史数据或无法识别时';
COMMENT ON COLUMN tenant_ledgers.biz_ref_type IS '业务引用类型order/refund/etc与 biz_ref_id 组成可选的结构化幂等/追溯键';
-- +goose StatementEnd

View File

@@ -1,21 +0,0 @@
-- +goose Up
-- +goose StatementBegin
-- contents补齐“简介/标签”字段,用于内容发布与列表展示
ALTER TABLE contents
ADD COLUMN IF NOT EXISTS summary varchar(256) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS tags jsonb NOT NULL DEFAULT '[]'::jsonb;
COMMENT ON COLUMN contents.summary IS '简介:用于列表/卡片展示的短文本;建议 <= 256 字符(由业务校验)';
COMMENT ON COLUMN contents.tags IS '标签JSON 数组(字符串列表);用于分类/检索与聚合展示';
CREATE INDEX IF NOT EXISTS ix_contents_tenant_tags ON contents(tenant_id);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS ix_contents_tenant_tags;
ALTER TABLE contents
DROP COLUMN IF EXISTS tags,
DROP COLUMN IF EXISTS summary;
-- +goose StatementEnd

View File

@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd