From 0fdd07130642e4740406560b5fc4d89bd50f1e7e Mon Sep 17 00:00:00 2001 From: Rogee Date: Fri, 9 Jan 2026 14:01:09 +0800 Subject: [PATCH] feat: sync initial schema migration --- .../migrations/20251227112605_init.sql | 355 +++++++++++++++++- backend/database/models/contents.gen.go | 2 +- backend/database/models/contents.query.gen.go | 186 ++++----- backend/database/models/media_assets.gen.go | 2 +- .../database/models/media_assets.query.gen.go | 2 +- 5 files changed, 449 insertions(+), 98 deletions(-) diff --git a/backend/database/migrations/20251227112605_init.sql b/backend/database/migrations/20251227112605_init.sql index b9c449e..256666d 100644 --- a/backend/database/migrations/20251227112605_init.sql +++ b/backend/database/migrations/20251227112605_init.sql @@ -1,9 +1,360 @@ -- +goose Up -- +goose StatementBegin -SELECT 'up SQL query'; +-- Users +CREATE TABLE IF NOT EXISTS users( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:用户唯一标识;约束:PK/不可为空 + username varchar(255) NOT NULL UNIQUE, -- 用户名:登录账号;用途:登录/展示;约束:唯一/非空/长度<=255 + password varchar(255) NOT NULL, -- 密码:加密存储;用途:登录校验;约束:非空/不得明文 + roles text[] DEFAULT '{user}', -- 角色:权限集合;用途:鉴权/授权;约束:枚举值数组/默认{user} + status varchar(50) DEFAULT 'active', -- 状态:账号状态;用途:禁用控制;约束:active/inactive/banned/默认active + metas jsonb DEFAULT '{}', -- 元数据:扩展信息;用途:灵活扩展;约束:JSON对象/默认{} + balance bigint DEFAULT 0, -- 余额:全局可用余额;用途:钱包/支付;约束:最小货币单位/默认0 + balance_frozen bigint DEFAULT 0, -- 冻结余额:冻结资金;用途:风控/结算;约束:最小货币单位/默认0 + verified_at timestamp with time zone, -- 实名认证时间:通过时间;用途:风控展示;约束:可空 + nickname varchar(255) DEFAULT '', -- 昵称:展示名称;用途:前台展示;约束:长度<=255/可空 + avatar varchar(512) DEFAULT '', -- 头像:URL地址;用途:展示;约束:长度<=512/可空 + gender varchar(32) DEFAULT 'secret', -- 性别:用户性别;用途:资料展示;约束:male/female/secret/默认secret + bio varchar(512) DEFAULT '', -- 简介:用户简介;用途:个人主页;约束:长度<=512/可空 + birthday date, -- 生日:YYYY-MM-DD;用途:资料展示;约束:可空 + location jsonb DEFAULT '{}', -- 位置:省市信息;用途:资料展示;约束:JSON对象/默认{} + points bigint DEFAULT 0, -- 积分:用户积分;用途:积分体系;约束:>=0/默认0 + phone varchar(32) DEFAULT '', -- 手机号:登录/验证;用途:OTP登录;约束:长度<=32/可空 + is_real_name_verified boolean DEFAULT FALSE, -- 实名认证:是否认证;用途:风控/展示;约束:布尔/默认false + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + deleted_at timestamp with time zone -- 删除时间:软删除;用途:逻辑删除;约束:可空 +); + +-- Tenants +CREATE TABLE IF NOT EXISTS tenants( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:租户唯一标识;约束:PK/不可为空 + user_id bigint NOT NULL, -- 创建者ID:关联users.id;用途:租户归属;约束:非空 + code varchar(64) NOT NULL UNIQUE, -- 租户代码:唯一编码;用途:URL/路由;约束:唯一/非空/长度<=64 + uuid uuid NOT NULL, -- UUID:全局唯一;用途:外部标识;约束:非空 + name varchar(128) NOT NULL, -- 租户名称:展示名;用途:后台展示;约束:非空/长度<=128 + status varchar(64) NOT NULL, -- 状态:租户状态;用途:启停控制;约束:pending_verify/verified/banned + config jsonb DEFAULT '{}', -- 配置:租户配置;用途:功能开关;约束:JSON对象/默认{} + expired_at timestamp with time zone, -- 过期时间:租户有效期;用途:到期控制;约束:可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +CREATE INDEX IF NOT EXISTS idx_tenants_user_id ON tenants(user_id); + +-- TenantUsers +CREATE TABLE IF NOT EXISTS tenant_users( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:成员关系唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:关联tenants.id;用途:多租户隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:关联users.id;用途:成员归属;约束:非空 + role text[] DEFAULT '{member}', -- 角色:成员角色;用途:权限控制;约束:member/tenant_admin/默认member + status varchar(50) DEFAULT 'verified', -- 状态:成员状态;用途:启停控制;约束:pending_verify/verified/banned + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + UNIQUE (tenant_id, user_id) -- 约束:同一租户内用户唯一 +); + +-- Contents +CREATE TABLE IF NOT EXISTS contents( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:内容唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:内容作者;用途:归属;约束:非空 + title varchar(255) NOT NULL, -- 标题:内容标题;用途:列表展示/搜索;约束:非空/长度<=255 + description text NOT NULL, -- 描述:详情说明;用途:详情展示;约束:非空 + status varchar(32) DEFAULT 'draft', -- 状态:内容状态;用途:审核/发布控制;约束:draft/reviewing/published/unpublished/blocked + visibility varchar(32) DEFAULT 'tenant_only', -- 可见性:可见范围;用途:权限控制;约束:public/tenant_only/private + preview_seconds int DEFAULT 60, -- 试看秒数:试看时长;用途:试看控制;约束:>=0/默认60 + preview_downloadable boolean DEFAULT FALSE, -- 试看下载:是否允许下载;用途:权限控制;约束:布尔/默认false + published_at timestamp with time zone, -- 发布时间:发布时刻;用途:排序/展示;约束:可空 + summary varchar(256) DEFAULT '', -- 简介:列表摘要;用途:卡片展示;约束:长度<=256/可空 + tags jsonb DEFAULT '[]', -- 标签:标签列表;用途:筛选;约束:JSON数组/默认[] + body text DEFAULT '', -- 内容正文:图文内容;用途:详情页;约束:可空 + genre varchar(64) DEFAULT '', -- 流派:内容类型;用途:分类筛选;约束:长度<=64/可空 + views int DEFAULT 0, -- 浏览量:浏览计数;用途:热度排序;约束:>=0/默认0 + likes int DEFAULT 0, -- 点赞数:点赞计数;用途:热度排序;约束:>=0/默认0 + key varchar(32) DEFAULT '', -- 调性:音乐调性;用途:展示/筛选;约束:长度<=32/可空 + is_pinned boolean DEFAULT FALSE, -- 置顶:是否置顶;用途:首页推荐;约束:布尔/默认false + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + deleted_at timestamp with time zone -- 删除时间:软删除;用途:逻辑删除;约束:可空 +); + +CREATE INDEX IF NOT EXISTS idx_contents_tenant_id ON contents(tenant_id); + +-- MediaAssets +CREATE TABLE IF NOT EXISTS media_assets( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:资源唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:资源上传者;用途:归属;约束:非空 + type varchar(32) DEFAULT 'video', -- 资源类型:媒体类型;用途:播放/展示;约束:video/audio/image + status varchar(32) DEFAULT 'uploaded', -- 处理状态:处理阶段;用途:转码流程;约束:uploaded/processing/ready/failed/deleted + provider varchar(64) NOT NULL, -- 存储提供方:存储类型;用途:访问/迁移;约束:非空 + bucket varchar(128) NOT NULL, -- 存储桶:桶名称;用途:定位对象;约束:非空 + object_key varchar(512) NOT NULL, -- 对象Key:对象路径;用途:访问对象;约束:非空/长度<=512 + meta jsonb DEFAULT '{}', -- 元数据:资源信息;用途:文件元数据;约束:JSON对象/默认{} + variant varchar(32) DEFAULT 'main', -- 产物类型:衍生版本;用途:主片/预览;约束:main/preview/默认main + source_asset_id bigint DEFAULT 0, -- 来源资源ID:派生来源;用途:追溯;约束:0表示无 + hash varchar(64) DEFAULT '', -- 文件哈希:用于去重;用途:秒传/去重;约束:长度<=64/可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + deleted_at timestamp with time zone -- 删除时间:软删除;用途:逻辑删除;约束:可空 +); + +CREATE INDEX IF NOT EXISTS idx_media_assets_hash ON media_assets (hash); + +-- ContentAssets +CREATE TABLE IF NOT EXISTS content_assets( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:内容资源关联;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:关联创建者;用途:归属;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:内容资源绑定;约束:非空 + asset_id bigint NOT NULL, -- 资源ID:关联资源;用途:内容资源绑定;约束:非空 + role varchar(32) DEFAULT 'main', -- 资源角色:资源用途;用途:封面/主文件;约束:main/cover/preview + sort int DEFAULT 0, -- 排序:展示排序;用途:前端排序;约束:>=0/默认0 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +-- ContentPrices +CREATE TABLE IF NOT EXISTS content_prices( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:价格记录;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:内容作者;用途:归属;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:定价归属;约束:非空 + currency varchar(16) DEFAULT 'CNY', -- 币种:货币类型;用途:计价;约束:ISO币种/默认CNY + price_amount bigint NOT NULL, -- 原价金额:最小货币单位;用途:计价;约束:>=0/非空 + discount_type varchar(16) DEFAULT 'none', -- 折扣类型:折扣规则;用途:促销;约束:none/percent/amount + discount_value bigint DEFAULT 0, -- 折扣值:折扣额度;用途:促销;约束:>=0/默认0 + discount_start_at timestamp with time zone, -- 折扣开始:起始时间;用途:促销;约束:可空 + discount_end_at timestamp with time zone, -- 折扣结束:结束时间;用途:促销;约束:可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + UNIQUE (tenant_id, content_id) -- 约束:同租户同内容仅一条价格记录 +); + +-- ContentAccess +CREATE TABLE IF NOT EXISTS content_access( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:内容权益记录;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:权益持有者;用途:权限判断;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:权限判断;约束:非空 + order_id bigint DEFAULT 0, -- 订单ID:关联订单;用途:溯源;约束:0表示无 + status varchar(16) DEFAULT 'active', -- 权益状态:使用状态;用途:权限判断;约束:active/revoked + revoked_at timestamp with time zone, -- 撤销时间:撤销时刻;用途:审计;约束:可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + UNIQUE (tenant_id, user_id, content_id) -- 约束:同用户同内容唯一权益 +); + +-- Orders +CREATE TABLE IF NOT EXISTS orders( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:订单唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:下单用户;用途:归属;约束:非空 + type varchar(32) DEFAULT 'content_purchase', -- 订单类型:业务类型;用途:业务分支;约束:content_purchase/recharge + status varchar(32) DEFAULT 'created', -- 订单状态:支付状态;用途:流程控制;约束:created/paid/refunded/closed + currency varchar(16) DEFAULT 'CNY', -- 币种:货币类型;用途:计价;约束:ISO币种/默认CNY + amount_original bigint NOT NULL, -- 原价金额:订单原价;用途:结算;约束:>=0/非空 + amount_discount bigint NOT NULL, -- 优惠金额:优惠合计;用途:结算;约束:>=0/非空 + amount_paid bigint NOT NULL, -- 实付金额:实际支付;用途:结算;约束:>=0/非空 + snapshot jsonb DEFAULT '{}', -- 订单快照:下单快照;用途:审计/对账;约束:JSON对象/默认{} + idempotency_key varchar(128) NOT NULL, -- 幂等键:请求幂等;用途:重复请求保护;约束:非空/长度<=128 + paid_at timestamp with time zone, -- 支付时间:支付成功时间;用途:对账;约束:可空 + refunded_at timestamp with time zone, -- 退款时间:退款时间;用途:对账;约束:可空 + refund_forced boolean DEFAULT FALSE, -- 强制退款:是否强制;用途:风控;约束:布尔/默认false + refund_operator_user_id bigint DEFAULT 0, -- 退款操作人:操作者ID;用途:审计;约束:0表示无 + refund_reason varchar(255) DEFAULT '', -- 退款原因:原因说明;用途:审计;约束:长度<=255/可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + coupon_id bigint DEFAULT 0 -- 优惠券ID:使用的券;用途:优惠计算;约束:0表示未使用 +); + +-- OrderItems +CREATE TABLE IF NOT EXISTS order_items( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:订单项唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:下单用户;用途:归属;约束:非空 + order_id bigint NOT NULL, -- 订单ID:关联订单;用途:订单明细;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:明细关联;约束:非空 + content_user_id bigint NOT NULL, -- 内容作者ID:作者用户;用途:分账;约束:非空 + amount_paid bigint NOT NULL, -- 实付金额:行实付;用途:结算;约束:>=0/非空 + snapshot jsonb DEFAULT '{}', -- 内容快照:下单内容快照;用途:审计;约束:JSON对象/默认{} + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +-- TenantLedgers +CREATE TABLE IF NOT EXISTS tenant_ledgers( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:流水唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:租户主或操作者;用途:归属;约束:非空 + order_id bigint DEFAULT 0, -- 订单ID:关联订单;用途:溯源;约束:0表示无 + type varchar(32) NOT NULL, -- 流水类型:业务类型;用途:账务分类;约束:枚举值/非空 + amount bigint NOT NULL, -- 流水金额:变动金额;用途:账务;约束:非空 + balance_before bigint NOT NULL, -- 变更前余额:可用余额;用途:审计;约束:非空 + balance_after bigint NOT NULL, -- 变更后余额:可用余额;用途:审计;约束:非空 + frozen_before bigint NOT NULL, -- 变更前冻结:冻结余额;用途:审计;约束:非空 + frozen_after bigint NOT NULL, -- 变更后冻结:冻结余额;用途:审计;约束:非空 + idempotency_key varchar(128) NOT NULL, -- 幂等键:幂等保障;用途:防重复入账;约束:非空 + remark varchar(255) NOT NULL, -- 备注:流水说明;用途:审计;约束:非空 + operator_user_id bigint DEFAULT 0, -- 操作者ID:人工操作人;用途:审计;约束:0表示无 + biz_ref_type varchar(32) DEFAULT '', -- 业务引用类型:外部关联;用途:对账;约束:长度<=32 + biz_ref_id bigint DEFAULT 0, -- 业务引用ID:外部关联;用途:对账;约束:0表示无 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +-- TenantInvites +CREATE TABLE IF NOT EXISTS tenant_invites( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:邀请记录;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:创建人;用途:溯源;约束:非空 + code varchar(64) NOT NULL, -- 邀请码:邀请码;用途:邀请加入;约束:非空/长度<=64 + status varchar(32) DEFAULT 'active', -- 邀请状态:是否可用;用途:控制邀请;约束:active/disabled/expired + max_uses int NOT NULL, -- 最大次数:可用次数;用途:限制;约束:>0/非空 + used_count int DEFAULT 0, -- 已使用:已用次数;用途:限制;约束:>=0/默认0 + expires_at timestamp with time zone, -- 过期时间:失效时间;用途:控制;约束:可空 + disabled_at timestamp with time zone, -- 禁用时间:禁用时刻;用途:审计;约束:可空 + disabled_operator_user_id bigint DEFAULT 0, -- 禁用操作人:操作者;用途:审计;约束:0表示无 + remark varchar(255) DEFAULT '', -- 备注:说明;用途:审计;约束:长度<=255/可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +-- TenantJoinRequests +CREATE TABLE IF NOT EXISTS tenant_join_requests( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:入驻申请;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:申请人;用途:溯源;约束:非空 + status varchar(32) DEFAULT 'pending', -- 申请状态:审核状态;用途:审核流程;约束:pending/approved/rejected + reason varchar(255) NOT NULL, -- 申请原因:申请说明;用途:审核;约束:非空/长度<=255 + decided_at timestamp with time zone, -- 处理时间:审核时间;用途:审计;约束:可空 + decided_operator_user_id bigint DEFAULT 0, -- 处理人ID:操作者;用途:审计;约束:0表示无 + decided_reason varchar(255) DEFAULT '', -- 处理说明:审核意见;用途:审计;约束:长度<=255/可空 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +-- Comments +CREATE TABLE IF NOT EXISTS comments( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:评论唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:评论者;用途:归属;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:评论归属;约束:非空 + reply_to bigint DEFAULT 0, -- 回复评论ID:0为一级;用途:楼中楼;约束:0表示无 + content text NOT NULL, -- 评论内容:评论文本;用途:展示;约束:非空 + likes int DEFAULT 0, -- 点赞数:点赞计数;用途:排序;约束:>=0/默认0 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW(), -- 更新时间:记录更新;用途:审计;约束:默认now() + deleted_at timestamp with time zone -- 删除时间:软删除;用途:逻辑删除;约束:可空 +); + +CREATE INDEX IF NOT EXISTS idx_comments_content_id ON comments(content_id); +CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id); + +-- User Content Actions (Like, Favorite) +CREATE TABLE IF NOT EXISTS user_content_actions( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:内容互动;约束:PK/不可为空 + user_id bigint NOT NULL, -- 用户ID:操作用户;用途:归属;约束:非空 + content_id bigint NOT NULL, -- 内容ID:关联内容;用途:互动对象;约束:非空 + type varchar(32) NOT NULL, -- 类型:互动类型;用途:点赞/收藏;约束:like/favorite + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + UNIQUE (user_id, content_id, type) -- 约束:同一用户同类型仅一次 +); + +-- User Comment Actions (Like) +CREATE TABLE IF NOT EXISTS user_comment_actions( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:评论互动;约束:PK/不可为空 + user_id bigint NOT NULL, -- 用户ID:操作用户;用途:归属;约束:非空 + comment_id bigint NOT NULL, -- 评论ID:关联评论;用途:互动对象;约束:非空 + type varchar(32) NOT NULL, -- 类型:互动类型;用途:点赞;约束:like + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + UNIQUE (user_id, comment_id, type) -- 约束:同一用户同类型仅一次 +); + +-- Payout Accounts +CREATE TABLE IF NOT EXISTS payout_accounts( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:提现账户;约束:PK/不可为空 + tenant_id bigint NOT NULL, -- 租户ID:多租户隔离;用途:数据隔离;约束:非空 + user_id bigint NOT NULL, -- 用户ID:账户持有人;用途:归属;约束:非空 + type varchar(32) NOT NULL, -- 类型:账户类型;用途:打款方式;约束:bank/alipay + name varchar(128) NOT NULL, -- 账户名称:开户行/支付宝名称;用途:打款;约束:非空/长度<=128 + account varchar(128) NOT NULL, -- 账号:银行卡/支付宝账号;用途:打款;约束:非空/长度<=128 + realname varchar(128) NOT NULL, -- 真实姓名:收款人;用途:打款;约束:非空/长度<=128 + created_at timestamp with time zone DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamp with time zone DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +CREATE INDEX IF NOT EXISTS idx_payout_accounts_tenant_id ON payout_accounts(tenant_id); + +-- Notifications +CREATE TABLE IF NOT EXISTS notifications( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:通知唯一标识;约束:PK/不可为空 + user_id bigint NOT NULL, -- 用户ID:接收者;用途:归属;约束:非空 + tenant_id bigint DEFAULT 0, -- 租户ID:来源租户;用途:来源标识;约束:0表示无 + type varchar(32) NOT NULL, -- 类型:通知类型;用途:分类展示;约束:system/order/audit/interaction + title varchar(255) NOT NULL, -- 标题:通知标题;用途:展示;约束:非空/长度<=255 + content text NOT NULL, -- 内容:通知内容;用途:展示;约束:非空 + is_read boolean DEFAULT FALSE, -- 已读:是否已读;用途:状态管理;约束:布尔/默认false + created_at timestamp with time zone DEFAULT NOW() -- 创建时间:记录创建;用途:审计;约束:默认now() +); + +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); + +-- Coupons +CREATE TABLE IF NOT EXISTS coupons( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:优惠券唯一标识;约束:PK/不可为空 + tenant_id bigint NOT NULL DEFAULT 0, -- 租户ID:适用租户;用途:范围控制;约束:0表示全局 + title varchar(255) NOT NULL, -- 标题:优惠券标题;用途:展示;约束:非空/长度<=255 + description text, -- 描述:优惠券说明;用途:展示;约束:可空 + type varchar(32) NOT NULL, -- 类型:优惠类型;用途:优惠计算;约束:fix_amount/discount + value bigint NOT NULL, -- 面值:优惠金额/折扣;用途:优惠计算;约束:>0 + min_order_amount bigint NOT NULL DEFAULT 0, -- 最低金额:使用门槛;用途:限制;约束:>=0/默认0 + max_discount bigint, -- 最高折扣:上限金额;用途:限制;约束:可空 + total_quantity integer NOT NULL DEFAULT 0, -- 总量:发行数量;用途:发放控制;约束:>=0/默认0 + used_quantity integer NOT NULL DEFAULT 0, -- 已用:已使用数量;用途:核销统计;约束:>=0/默认0 + start_at timestamptz, -- 开始时间:生效时间;用途:有效期控制;约束:可空 + end_at timestamptz, -- 结束时间:失效时间;用途:有效期控制;约束:可空 + created_at timestamptz NOT NULL DEFAULT NOW(), -- 创建时间:记录创建;用途:审计;约束:默认now() + updated_at timestamptz NOT NULL DEFAULT NOW() -- 更新时间:记录更新;用途:审计;约束:默认now() +); + +CREATE INDEX IF NOT EXISTS idx_coupons_tenant_id ON coupons(tenant_id); + +-- UserCoupons +CREATE TABLE IF NOT EXISTS user_coupons( + id bigserial PRIMARY KEY, -- 主键ID:自增;用途:用户券唯一标识;约束:PK/不可为空 + user_id bigint NOT NULL, -- 用户ID:领券用户;用途:归属;约束:非空 + coupon_id bigint NOT NULL, -- 优惠券ID:关联优惠券;用途:关联;约束:非空 + order_id bigint, -- 订单ID:使用订单;用途:核销溯源;约束:可空 + status varchar(32) NOT NULL DEFAULT 'unused', -- 状态:使用状态;用途:核销;约束:unused/used/expired + used_at timestamptz, -- 使用时间:核销时间;用途:审计;约束:可空 + created_at timestamptz NOT NULL DEFAULT NOW() -- 创建时间:记录创建;用途:审计;约束:默认now() +); + +CREATE INDEX IF NOT EXISTS idx_user_coupons_user_id ON user_coupons(user_id); +CREATE INDEX IF NOT EXISTS idx_user_coupons_coupon_id ON user_coupons(coupon_id); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin -SELECT 'down SQL query'; +DROP TABLE IF EXISTS user_coupons; +DROP TABLE IF EXISTS coupons; +DROP TABLE IF EXISTS notifications; +DROP TABLE IF EXISTS payout_accounts; +DROP TABLE IF EXISTS user_comment_actions; +DROP TABLE IF EXISTS user_content_actions; +DROP TABLE IF EXISTS comments; +DROP TABLE IF EXISTS tenant_join_requests; +DROP TABLE IF EXISTS tenant_invites; +DROP TABLE IF EXISTS tenant_ledgers; +DROP TABLE IF EXISTS order_items; +DROP TABLE IF EXISTS orders; +DROP TABLE IF EXISTS content_access; +DROP TABLE IF EXISTS content_prices; +DROP TABLE IF EXISTS content_assets; +DROP TABLE IF EXISTS media_assets; +DROP TABLE IF EXISTS contents; +DROP TABLE IF EXISTS tenant_users; +DROP TABLE IF EXISTS tenants; +DROP TABLE IF EXISTS users; -- +goose StatementEnd diff --git a/backend/database/models/contents.gen.go b/backend/database/models/contents.gen.go index a3de8e8..fc251f6 100644 --- a/backend/database/models/contents.gen.go +++ b/backend/database/models/contents.gen.go @@ -40,9 +40,9 @@ type Content struct { DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` Key string `gorm:"column:key;type:character varying(32);comment:Musical key/tone" json:"key"` // Musical key/tone IsPinned bool `gorm:"column:is_pinned;type:boolean;comment:Whether content is pinned/featured" json:"is_pinned"` // Whether content is pinned/featured - Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"` Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"` ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"` + Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/contents.query.gen.go b/backend/database/models/contents.query.gen.go index ddbf1b6..d462865 100644 --- a/backend/database/models/contents.query.gen.go +++ b/backend/database/models/contents.query.gen.go @@ -46,12 +46,6 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery { _contentQuery.DeletedAt = field.NewField(tableName, "deleted_at") _contentQuery.Key = field.NewString(tableName, "key") _contentQuery.IsPinned = field.NewBool(tableName, "is_pinned") - _contentQuery.Comments = contentQueryHasManyComments{ - db: db.Session(&gorm.Session{}), - - RelationField: field.NewRelation("Comments", "Comment"), - } - _contentQuery.Author = contentQueryBelongsToAuthor{ db: db.Session(&gorm.Session{}), @@ -64,6 +58,12 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery { RelationField: field.NewRelation("ContentAssets", "ContentAsset"), } + _contentQuery.Comments = contentQueryHasManyComments{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Comments", "Comment"), + } + _contentQuery.fillFieldMap() return _contentQuery @@ -94,12 +94,12 @@ type contentQuery struct { DeletedAt field.Field Key field.String // Musical key/tone IsPinned field.Bool // Whether content is pinned/featured - Comments contentQueryHasManyComments - - Author contentQueryBelongsToAuthor + Author contentQueryBelongsToAuthor ContentAssets contentQueryHasManyContentAssets + Comments contentQueryHasManyComments + fieldMap map[string]field.Expr } @@ -195,104 +195,23 @@ func (c *contentQuery) fillFieldMap() { func (c contentQuery) clone(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool) - c.Comments.db = db.Session(&gorm.Session{Initialized: true}) - c.Comments.db.Statement.ConnPool = db.Statement.ConnPool c.Author.db = db.Session(&gorm.Session{Initialized: true}) c.Author.db.Statement.ConnPool = db.Statement.ConnPool c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true}) c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool + c.Comments.db = db.Session(&gorm.Session{Initialized: true}) + c.Comments.db.Statement.ConnPool = db.Statement.ConnPool return c } func (c contentQuery) replaceDB(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceDB(db) - c.Comments.db = db.Session(&gorm.Session{}) c.Author.db = db.Session(&gorm.Session{}) c.ContentAssets.db = db.Session(&gorm.Session{}) + c.Comments.db = db.Session(&gorm.Session{}) return c } -type contentQueryHasManyComments struct { - db *gorm.DB - - field.RelationField -} - -func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments { - if len(conds) == 0 { - return &a - } - - exprs := make([]clause.Expression, 0, len(conds)) - for _, cond := range conds { - exprs = append(exprs, cond.BeCond().(clause.Expression)) - } - a.db = a.db.Clauses(clause.Where{Exprs: exprs}) - return &a -} - -func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments { - a.db = a.db.WithContext(ctx) - return &a -} - -func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments { - a.db = a.db.Session(session) - return &a -} - -func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx { - return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())} -} - -func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments { - a.db = a.db.Unscoped() - return &a -} - -type contentQueryHasManyCommentsTx struct{ tx *gorm.Association } - -func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) { - return result, a.tx.Find(&result) -} - -func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Append(targetValues...) -} - -func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Replace(targetValues...) -} - -func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) { - targetValues := make([]interface{}, len(values)) - for i, v := range values { - targetValues[i] = v - } - return a.tx.Delete(targetValues...) -} - -func (a contentQueryHasManyCommentsTx) Clear() error { - return a.tx.Clear() -} - -func (a contentQueryHasManyCommentsTx) Count() int64 { - return a.tx.Count() -} - -func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx { - a.tx = a.tx.Unscoped() - return &a -} - type contentQueryBelongsToAuthor struct { db *gorm.DB @@ -455,6 +374,87 @@ func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyConte return &a } +type contentQueryHasManyComments struct { + db *gorm.DB + + field.RelationField +} + +func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments { + a.db = a.db.Session(session) + return &a +} + +func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx { + return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments { + a.db = a.db.Unscoped() + return &a +} + +type contentQueryHasManyCommentsTx struct{ tx *gorm.Association } + +func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) { + return result, a.tx.Find(&result) +} + +func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Clear() error { + return a.tx.Clear() +} + +func (a contentQueryHasManyCommentsTx) Count() int64 { + return a.tx.Count() +} + +func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx { + a.tx = a.tx.Unscoped() + return &a +} + type contentQueryDo struct{ gen.DO } func (c contentQueryDo) Debug() *contentQueryDo { diff --git a/backend/database/models/media_assets.gen.go b/backend/database/models/media_assets.gen.go index 731357c..ef0e2fa 100644 --- a/backend/database/models/media_assets.gen.go +++ b/backend/database/models/media_assets.gen.go @@ -34,7 +34,7 @@ type MediaAsset struct { CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` - Hash string `gorm:"column:hash;type:character varying(64);comment:File SHA-256 hash" json:"hash"` // File SHA-256 hash + Hash string `gorm:"column:hash;type:character varying(64);comment:文件 MD5 哈希" json:"hash"` // 文件 MD5 哈希 } // Quick operations without importing query package diff --git a/backend/database/models/media_assets.query.gen.go b/backend/database/models/media_assets.query.gen.go index 4cf05a0..cd8adc4 100644 --- a/backend/database/models/media_assets.query.gen.go +++ b/backend/database/models/media_assets.query.gen.go @@ -64,7 +64,7 @@ type mediaAssetQuery struct { CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field - Hash field.String // File SHA-256 hash + Hash field.String // 文件 MD5 哈希 fieldMap map[string]field.Expr }