0001_init.up.sql 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. -- 0001_init
  2. --
  3. -- Haven 初始 schema。依据:
  4. -- docs/backend/data-model.md(表、索引、枚举)
  5. -- docs/backend/auth.md 第三节 / 3.1 / 四 / 五 / 九(sessions、pending_logins、
  6. -- trusted_devices、tokens、users)
  7. -- docs/backend/audit.md 第二节 / 第三节(audit_events)
  8. --
  9. -- 建表顺序遵循 data-model.md 第一节:
  10. -- users -> sessions / pending_logins / trusted_devices / tokens / audit_events
  11. -- -> notes / tags / portfolio_items
  12. -- -> note_tags / portfolio_repos
  13. --
  14. -- 本文件由部署脚本执行,后端启动时不自动跑 migration
  15. -- (docs/architecture.md 5.6)。
  16. -- 搜索依赖 pg_trgm(data-model.md 第一节)。
  17. -- PostgreSQL >= 15 自带 gen_random_uuid(),无需 pgcrypto。
  18. CREATE EXTENSION IF NOT EXISTS pg_trgm;
  19. -- ---------------------------------------------------------------------------
  20. -- users(字段以 docs/backend/auth.md 第九节为准)
  21. -- ---------------------------------------------------------------------------
  22. CREATE TABLE users (
  23. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  24. username text NOT NULL,
  25. password_hash text NOT NULL,
  26. totp_secret text NULL DEFAULT NULL,
  27. totp_enabled boolean NOT NULL DEFAULT false,
  28. created_at timestamptz NOT NULL DEFAULT now(),
  29. updated_at timestamptz NOT NULL DEFAULT now()
  30. );
  31. -- username 不区分大小写唯一(auth.md 第九节规则 3)。
  32. -- 只建小写唯一索引,不再额外加普通 UNIQUE,避免 Admin / admin 同时存在。
  33. CREATE UNIQUE INDEX idx_users_username_lower ON users (lower(username));
  34. -- ---------------------------------------------------------------------------
  35. -- sessions(auth.md 第三节)
  36. -- ---------------------------------------------------------------------------
  37. CREATE TABLE sessions (
  38. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  39. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  40. token_hash text NOT NULL UNIQUE,
  41. expires_at timestamptz NOT NULL,
  42. last_used_at timestamptz NOT NULL,
  43. user_agent text NOT NULL DEFAULT '',
  44. created_at timestamptz NOT NULL DEFAULT now()
  45. );
  46. -- ---------------------------------------------------------------------------
  47. -- pending_logins(auth.md 3.1)
  48. -- ---------------------------------------------------------------------------
  49. CREATE TABLE pending_logins (
  50. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  51. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  52. token_hash text NOT NULL UNIQUE,
  53. expires_at timestamptz NOT NULL,
  54. created_at timestamptz NOT NULL DEFAULT now()
  55. );
  56. -- ---------------------------------------------------------------------------
  57. -- trusted_devices(auth.md 第四节)
  58. -- ---------------------------------------------------------------------------
  59. CREATE TABLE trusted_devices (
  60. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  61. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  62. token_hash text NOT NULL UNIQUE,
  63. device_name text NOT NULL DEFAULT '',
  64. expires_at timestamptz NOT NULL,
  65. last_used_at timestamptz NOT NULL,
  66. created_at timestamptz NOT NULL DEFAULT now()
  67. );
  68. -- ---------------------------------------------------------------------------
  69. -- tokens(auth.md 第五节)
  70. -- ---------------------------------------------------------------------------
  71. CREATE TABLE tokens (
  72. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  73. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  74. name text NOT NULL,
  75. token_hash text NOT NULL UNIQUE,
  76. token_prefix text NOT NULL,
  77. scope text NOT NULL CHECK (scope IN ('read', 'read_write')),
  78. expires_at timestamptz NULL,
  79. last_used_at timestamptz NULL,
  80. revoked_at timestamptz NULL,
  81. created_at timestamptz NOT NULL DEFAULT now()
  82. );
  83. -- ---------------------------------------------------------------------------
  84. -- audit_events(audit.md 第二节、第三节)
  85. -- 只追加,不修改,不删除。
  86. -- ---------------------------------------------------------------------------
  87. CREATE TABLE audit_events (
  88. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  89. actor_user_id uuid NULL REFERENCES users (id) ON DELETE SET NULL,
  90. actor_username text NOT NULL DEFAULT '',
  91. event_type text NOT NULL CHECK (event_type IN (
  92. 'login_success',
  93. 'login_failure',
  94. 'logout',
  95. 'totp_bound',
  96. 'totp_disabled',
  97. 'password_changed',
  98. 'session_revoked',
  99. 'trusted_device_added',
  100. 'trusted_device_revoked',
  101. 'token_created',
  102. 'token_revoked',
  103. 'note_published',
  104. 'note_unpublished'
  105. )),
  106. -- 'null' 为字符串字面量,表示“无对象”,与 SQL NULL 区分(audit.md 第三节说明)
  107. target_type text NULL CHECK (target_type IN (
  108. 'null', 'user', 'session', 'trusted_device', 'token', 'note'
  109. )),
  110. target_id uuid NULL,
  111. metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
  112. ip text NOT NULL DEFAULT '',
  113. user_agent text NOT NULL DEFAULT '',
  114. created_at timestamptz NOT NULL DEFAULT now()
  115. );
  116. CREATE INDEX idx_audit_events_created_at ON audit_events (created_at DESC);
  117. CREATE INDEX idx_audit_events_event_type ON audit_events (event_type, created_at DESC);
  118. CREATE INDEX idx_audit_events_actor ON audit_events (actor_user_id, created_at DESC)
  119. WHERE actor_user_id IS NOT NULL;
  120. -- ---------------------------------------------------------------------------
  121. -- notes(data-model.md 第三节)
  122. -- ---------------------------------------------------------------------------
  123. CREATE TABLE notes (
  124. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  125. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  126. title text NOT NULL,
  127. content text NOT NULL DEFAULT '',
  128. visibility text NOT NULL DEFAULT 'private'
  129. CHECK (visibility IN ('private', 'public')),
  130. published_at timestamptz NULL DEFAULT NULL,
  131. -- 搜索用合并字段,数据库自动维护,应用层只读不写(data-model.md 第三节规则 6)
  132. search_text text GENERATED ALWAYS AS (title || ' ' || content) STORED,
  133. created_at timestamptz NOT NULL DEFAULT now(),
  134. updated_at timestamptz NOT NULL DEFAULT now()
  135. );
  136. -- 公开列表:按用户 + 发布时间倒序
  137. CREATE INDEX idx_notes_public ON notes (user_id, published_at DESC)
  138. WHERE visibility = 'public' AND published_at IS NOT NULL;
  139. -- 私有工作台:按用户 + 最近更新排序
  140. CREATE INDEX idx_notes_updated_at ON notes (user_id, updated_at DESC);
  141. -- 模糊搜索:search_text trigram 索引
  142. CREATE INDEX idx_notes_search_trgm ON notes USING gin (search_text gin_trgm_ops);
  143. -- ---------------------------------------------------------------------------
  144. -- tags(data-model.md 第四节)
  145. -- ---------------------------------------------------------------------------
  146. CREATE TABLE tags (
  147. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  148. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  149. name text NOT NULL,
  150. created_at timestamptz NOT NULL DEFAULT now()
  151. );
  152. -- 同一用户下标签名不区分大小写唯一
  153. CREATE UNIQUE INDEX idx_tags_user_name_lower ON tags (user_id, lower(name));
  154. -- 标签名模糊搜索
  155. CREATE INDEX idx_tags_name_trgm ON tags USING gin (name gin_trgm_ops);
  156. -- ---------------------------------------------------------------------------
  157. -- portfolio_items(data-model.md 第六节)
  158. -- ---------------------------------------------------------------------------
  159. CREATE TABLE portfolio_items (
  160. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  161. user_id uuid NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  162. title text NOT NULL,
  163. description text NOT NULL DEFAULT '',
  164. sort_order int NOT NULL DEFAULT 0,
  165. created_at timestamptz NOT NULL DEFAULT now()
  166. );
  167. CREATE INDEX idx_portfolio_items_sort ON portfolio_items (user_id, sort_order);
  168. -- ---------------------------------------------------------------------------
  169. -- note_tags(data-model.md 第五节)
  170. -- 无 user_id:归属由 notes.user_id 与 tags.user_id 在应用层校验。
  171. -- ---------------------------------------------------------------------------
  172. CREATE TABLE note_tags (
  173. note_id uuid NOT NULL REFERENCES notes (id) ON DELETE CASCADE,
  174. tag_id uuid NOT NULL REFERENCES tags (id) ON DELETE CASCADE,
  175. PRIMARY KEY (note_id, tag_id)
  176. );
  177. -- 复合主键已覆盖 note_id 开头的查询;单独为 tag_id 建索引,支持“按标签查笔记”
  178. CREATE INDEX idx_note_tags_tag_id ON note_tags (tag_id);
  179. -- ---------------------------------------------------------------------------
  180. -- portfolio_repos(data-model.md 第七节)
  181. -- 无 user_id:归属通过 portfolio_items 间接确定。
  182. -- ---------------------------------------------------------------------------
  183. CREATE TABLE portfolio_repos (
  184. id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  185. portfolio_item_id uuid NOT NULL REFERENCES portfolio_items (id) ON DELETE CASCADE,
  186. platform text NOT NULL CHECK (platform IN ('gogs', 'github')),
  187. repo_path text NOT NULL,
  188. is_primary boolean NOT NULL DEFAULT false
  189. );
  190. -- 同一作品下平台 + 路径唯一
  191. CREATE UNIQUE INDEX idx_portfolio_repos_unique
  192. ON portfolio_repos (portfolio_item_id, platform, repo_path);
  193. -- 每个作品最多一个主链接
  194. CREATE UNIQUE INDEX idx_portfolio_repos_primary
  195. ON portfolio_repos (portfolio_item_id)
  196. WHERE is_primary = true;
苏ICP备2026082406号
苏公网安备 待审核