@laitszkin/apollo-toolkit 3.9.7 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +6 -0
  4. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  5. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  6. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  7. package/cjk-pdf/agents/openai.yaml +5 -0
  8. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  9. package/generate-spec/SKILL.md +24 -4
  10. package/generate-spec/agents/openai.yaml +1 -0
  11. package/generate-spec/references/TEMPLATE_SPEC.md +98 -0
  12. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  13. package/init-project-html/SKILL.md +181 -0
  14. package/init-project-html/agents/openai.yaml +13 -0
  15. package/init-project-html/references/TEMPLATE_SPEC.md +98 -0
  16. package/init-project-html/references/architecture-page.template.html +35 -0
  17. package/init-project-html/references/architecture.css +1059 -0
  18. package/init-project-html/sample-demo/resources/project-architecture/assets/architecture.css +1059 -0
  19. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/index.html +54 -0
  20. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-code-generator.html +165 -0
  21. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/invite-issuance-service.html +198 -0
  22. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/postgresql.html +165 -0
  23. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/public-api.html +152 -0
  24. package/init-project-html/sample-demo/resources/project-architecture/features/get-invite-codes/web-get-invite-ui.html +140 -0
  25. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/index.html +53 -0
  26. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/postgresql.html +161 -0
  27. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/public-api.html +145 -0
  28. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/registration-service.html +190 -0
  29. package/init-project-html/sample-demo/resources/project-architecture/features/invite-code-registration/web-register-ui.html +140 -0
  30. package/init-project-html/sample-demo/resources/project-architecture/index.html +337 -0
  31. package/init-project-html/scripts/architecture.js +496 -0
  32. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  33. package/lib/cli.js +2 -0
  34. package/lib/tool-runner.js +7 -0
  35. package/merge-conflict-resolver/agents/openai.yaml +5 -0
  36. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  37. package/package.json +1 -1
  38. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  39. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  40. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  41. package/spec-to-project-html/SKILL.md +116 -0
  42. package/spec-to-project-html/agents/openai.yaml +12 -0
  43. package/spec-to-project-html/references/TEMPLATE_SPEC.md +98 -0
  44. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
@@ -0,0 +1,152 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>子模組 · public-api — 獲取邀請碼</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="../../assets/architecture.css" />
14
+ </head>
15
+ <body>
16
+ <main class="atlas-page atlas-page--feature">
17
+ <nav class="breadcrumb" aria-label="階層導航">
18
+ <a href="../../index.html">宏觀架構</a> · <a href="./index.html">獲取邀請碼</a> · <span>public-api</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Submodule · public-api</p>
23
+ <h1 class="atlas-title">public-api(iss)</h1>
24
+ <p class="atlas-meta">自身職責:HTTP 邊界:認證解析、輸入綁定、把網域結果映射為狀態碼。</p>
25
+ </header>
26
+
27
+ <section class="sub-io">
28
+ <h2>函式 I/O</h2>
29
+ <table>
30
+ <thead><tr><th>函式</th><th>簽名</th><th>副作用</th><th>用途</th></tr></thead>
31
+ <tbody>
32
+ <tr>
33
+ <td><code>AuthMiddleware</code></td>
34
+ <td class="sub-io__signature">
35
+ <strong>in:</strong> <code>http.Request</code><br>
36
+ <strong>out:</strong> <code>Principal</code> | <code>401</code>
37
+ </td>
38
+ <td><span class="sub-io__side sub-io__side--io">io</span> 解 cookie / JWT</td>
39
+ <td>驗證並注入 <code>Principal.UserID</code>。</td>
40
+ </tr>
41
+ <tr>
42
+ <td><code>CreateInviteCodeHandler</code></td>
43
+ <td class="sub-io__signature">
44
+ <strong>in:</strong> <code>http.Request</code>, <code>Principal</code><br>
45
+ <strong>out:</strong> <code>http.Response</code>
46
+ </td>
47
+ <td><span class="sub-io__side sub-io__side--io">io</span> 寫 body</td>
48
+ <td>路由綁定的對外入口。</td>
49
+ </tr>
50
+ <tr>
51
+ <td><code>mapDomainError</code></td>
52
+ <td class="sub-io__signature">
53
+ <strong>in:</strong> <code>error</code><br>
54
+ <strong>out:</strong> <code>(httpStatus, ErrorBody)</code>
55
+ </td>
56
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
57
+ <td>quota → 429(含 <code>Retry-After</code>)等對映。</td>
58
+ </tr>
59
+ <tr>
60
+ <td><code>writeCreated</code></td>
61
+ <td class="sub-io__signature">
62
+ <strong>in:</strong> <code>NewCode</code>, <code>http.ResponseWriter</code><br>
63
+ <strong>out:</strong> —
64
+ </td>
65
+ <td><span class="sub-io__side sub-io__side--write">io</span> 寫 201 body</td>
66
+ <td>序列化新碼 + metadata。</td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ </section>
71
+
72
+ <section class="sub-vars">
73
+ <h2>變數與業務用途</h2>
74
+ <p class="sub-vars__intro">HTTP 邊界子模組持有的識別子;本子模組的責任是「正確識別調用者」與「正確翻譯網域結果為協議語意」。</p>
75
+ <table>
76
+ <thead><tr><th>變數</th><th>型別</th><th>作用域</th><th>業務用途</th></tr></thead>
77
+ <tbody>
78
+ <tr>
79
+ <td class="sub-vars__name">http.Request</td>
80
+ <td class="sub-vars__type">request</td>
81
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
82
+ <td>邊界輸入;只在 handler 期間有效。</td>
83
+ </tr>
84
+ <tr>
85
+ <td class="sub-vars__name">Principal.UserID</td>
86
+ <td class="sub-vars__type">UUID</td>
87
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
88
+ <td>由 middleware 注入;業務上代表「為誰生成邀請碼」;<strong>禁止</strong>從 body 取以避免使用者冒名為他人發碼。</td>
89
+ </tr>
90
+ <tr>
91
+ <td class="sub-vars__name">httpStatus</td>
92
+ <td class="sub-vars__type">int</td>
93
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
94
+ <td><code>mapDomainError</code> 的輸出之一;<code>ErrQuotaExceeded</code> 必須對映成 <code>429</code> 才能搭配 <code>Retry-After</code> 讓 UI 知道是節流。</td>
95
+ </tr>
96
+ <tr>
97
+ <td class="sub-vars__name">ErrorBody</td>
98
+ <td class="sub-vars__type">{code, message}</td>
99
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
100
+ <td>結構化錯誤體;<code>code</code> 給 UI 對映本地化文字(例如「今日額度用盡」)。</td>
101
+ </tr>
102
+ <tr>
103
+ <td class="sub-vars__name">Retry-After</td>
104
+ <td class="sub-vars__type">int seconds</td>
105
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
106
+ <td>節流提示 header;UI 透過它倒數禁用按鈕,避免使用者反覆 429。</td>
107
+ </tr>
108
+ <tr>
109
+ <td class="sub-vars__name">NewCode</td>
110
+ <td class="sub-vars__type">string</td>
111
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
112
+ <td>網域回傳的新碼;序列化為 201 body 給使用者複製分享。</td>
113
+ </tr>
114
+ <tr>
115
+ <td class="sub-vars__name">http.ResponseWriter</td>
116
+ <td class="sub-vars__type">writer</td>
117
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
118
+ <td>對外副作用通道;保證一次回應只寫一次。</td>
119
+ </tr>
120
+ </tbody>
121
+ </table>
122
+ </section>
123
+
124
+ <section class="sub-dataflow">
125
+ <h2>內部資料流</h2>
126
+ <ol>
127
+ <li><code>Request</code> → <code>AuthMiddleware</code> → 注入 <code>Principal{UserID}</code>,失敗即 <code>401</code>。</li>
128
+ <li><code>Principal.UserID</code> 作為網域層輸入;本子模組不持有 token 明文。</li>
129
+ <li>從網域層取得 <code>NewCode | error</code>。</li>
130
+ <li><code>NewCode</code> → <code>writeCreated</code>;<code>error</code> → <code>mapDomainError</code>。</li>
131
+ </ol>
132
+ </section>
133
+
134
+ <section class="sub-errors">
135
+ <h2>HTTP 對映</h2>
136
+ <table>
137
+ <thead><tr><th>條件</th><th>HTTP</th></tr></thead>
138
+ <tbody>
139
+ <tr><td>無 session</td><td><code>401</code></td></tr>
140
+ <tr><td>網域 <code>ErrQuotaExceeded</code></td><td><code>429</code> + <code>Retry-After</code></td></tr>
141
+ <tr><td>網域 <code>ErrForbidden</code></td><td><code>403</code></td></tr>
142
+ <tr><td>網域 <code>ErrTransient</code></td><td><code>503</code></td></tr>
143
+ </tbody>
144
+ </table>
145
+ </section>
146
+
147
+ <footer class="atlas-meta" style="margin-top: 2rem">
148
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
149
+ </footer>
150
+ </main>
151
+ </body>
152
+ </html>
@@ -0,0 +1,140 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>子模組 · web-get-invite-ui — 獲取邀請碼</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="../../assets/architecture.css" />
14
+ </head>
15
+ <body>
16
+ <main class="atlas-page atlas-page--feature">
17
+ <nav class="breadcrumb" aria-label="階層導航">
18
+ <a href="../../index.html">宏觀架構</a> · <a href="./index.html">獲取邀請碼</a> · <span>web-get-invite-ui</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Submodule · web-get-invite-ui</p>
23
+ <h1 class="atlas-title">web-get-invite-ui</h1>
24
+ <p class="atlas-meta">自身職責:登入後顯示「我的邀請碼」、列表載入、生成按鈕。本頁只談自身函式 I/O 與內部資料流。</p>
25
+ </header>
26
+
27
+ <section class="sub-io">
28
+ <h2>函式 I/O</h2>
29
+ <table>
30
+ <thead><tr><th>函式</th><th>簽名</th><th>副作用</th><th>用途</th></tr></thead>
31
+ <tbody>
32
+ <tr>
33
+ <td><code>useInviteCodes()</code></td>
34
+ <td class="sub-io__signature">
35
+ <strong>in:</strong> —<br>
36
+ <strong>out:</strong> <code>{list, isLoading, refresh}</code>
37
+ </td>
38
+ <td><span class="sub-io__side sub-io__side--io">network</span> GET 已發放碼</td>
39
+ <td>查詢鍵/快取列表。</td>
40
+ </tr>
41
+ <tr>
42
+ <td><code>createInviteCode</code></td>
43
+ <td class="sub-io__signature">
44
+ <strong>in:</strong> —<br>
45
+ <strong>out:</strong> <code>Result&lt;NewCode, UiError&gt;</code>
46
+ </td>
47
+ <td><span class="sub-io__side sub-io__side--io">network</span> POST</td>
48
+ <td>把伺服器產生的新碼樂觀 prepend 到列表。</td>
49
+ </tr>
50
+ <tr>
51
+ <td><code>copyCodeToClipboard</code></td>
52
+ <td class="sub-io__signature">
53
+ <strong>in:</strong> <code>code: string</code><br>
54
+ <strong>out:</strong> <code>void</code>
55
+ </td>
56
+ <td><span class="sub-io__side sub-io__side--io">browser</span> clipboard API</td>
57
+ <td>使用者點復制。</td>
58
+ </tr>
59
+ <tr>
60
+ <td><code>formatQuotaError</code></td>
61
+ <td class="sub-io__signature">
62
+ <strong>in:</strong> <code>RetryAfter | null</code><br>
63
+ <strong>out:</strong> <code>UiMessage</code>
64
+ </td>
65
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
66
+ <td>把 429 訊息翻成本地文字。</td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ </section>
71
+
72
+ <section class="sub-vars">
73
+ <h2>變數與業務用途</h2>
74
+ <p class="sub-vars__intro">本子模組以前端組件狀態為主;下表為決定使用者體驗的識別子。</p>
75
+ <table>
76
+ <thead><tr><th>變數</th><th>型別</th><th>作用域</th><th>業務用途</th></tr></thead>
77
+ <tbody>
78
+ <tr>
79
+ <td class="sub-vars__name">list</td>
80
+ <td class="sub-vars__type">InviteCode[]</td>
81
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
82
+ <td>使用者已發放的邀請碼清單;驅動畫面呈現與「複製」按鈕可用性;保留順序讓使用者能識別最近一次取得的碼。</td>
83
+ </tr>
84
+ <tr>
85
+ <td class="sub-vars__name">isLoading</td>
86
+ <td class="sub-vars__type">boolean</td>
87
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
88
+ <td>生成 API 進行中旗標;防止連按重複生成造成額度被無意義消耗。</td>
89
+ </tr>
90
+ <tr>
91
+ <td class="sub-vars__name">retryAfter</td>
92
+ <td class="sub-vars__type">number seconds | null</td>
93
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
94
+ <td>後端 429 帶回的下次可生成時間;按鈕禁用倒數的依據,業務上提示使用者「節流而非系統故障」。</td>
95
+ </tr>
96
+ <tr>
97
+ <td class="sub-vars__name">Result</td>
98
+ <td class="sub-vars__type">{ok: NewCode} | {err: UiError}</td>
99
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
100
+ <td>一次「生成」呼叫結果;驅動是否要把新碼樂觀插入 <code>list</code>,或顯示錯誤 banner。</td>
101
+ </tr>
102
+ <tr>
103
+ <td class="sub-vars__name">UiMessage</td>
104
+ <td class="sub-vars__type">{lang_tag, text}</td>
105
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
106
+ <td>額度/網路錯誤的本地化呈現;使用者透過此訊息知道是自己的額度問題還是系統暫時不可用。</td>
107
+ </tr>
108
+ </tbody>
109
+ </table>
110
+ </section>
111
+
112
+ <section class="sub-dataflow">
113
+ <h2>內部資料流</h2>
114
+ <ol>
115
+ <li>頁面掛載 → <code>useInviteCodes</code> 取得 <code>list</code> 顯示。</li>
116
+ <li>使用者點生成 → <code>createInviteCode</code>:UI <code>idle → loading</code>。</li>
117
+ <li><code>Result.ok</code> → 把 <code>NewCode</code> prepend 到 <code>list</code>(保持其餘元素穩定)。</li>
118
+ <li><code>Result.err</code> 為 quota → <code>formatQuotaError</code> 顯示 <code>UiMessage</code>,<code>list</code> 不變。</li>
119
+ <li>使用者選擇 <code>copyCodeToClipboard(code)</code>,不影響資料模型。</li>
120
+ </ol>
121
+ </section>
122
+
123
+ <section class="sub-errors">
124
+ <h2>本子模組可暴露的錯誤</h2>
125
+ <table>
126
+ <thead><tr><th>來源</th><th>UI 行為</th></tr></thead>
127
+ <tbody>
128
+ <tr><td>未登入(401)</td><td>路由守衛攔截 → 導向登入;本子模組不顯示業務錯誤。</td></tr>
129
+ <tr><td>quota(429)</td><td><code>formatQuotaError</code> banner;按鈕短暫禁用至 <code>retryAfter</code>。</td></tr>
130
+ <tr><td>網路錯誤</td><td>泛用提示;可重試。</td></tr>
131
+ </tbody>
132
+ </table>
133
+ </section>
134
+
135
+ <footer class="atlas-meta" style="margin-top: 2rem">
136
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
137
+ </footer>
138
+ </main>
139
+ </body>
140
+ </html>
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>邀請碼註冊 — 功能模組總覽</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="../../assets/architecture.css" />
14
+ </head>
15
+ <body>
16
+ <main class="atlas-page atlas-page--feature">
17
+ <nav class="breadcrumb" aria-label="階層導航">
18
+ <a href="../../index.html">宏觀架構</a> · <span>邀請碼註冊(功能模組)</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Feature module · invite-code-registration</p>
23
+ <h1 class="atlas-title">邀請碼註冊</h1>
24
+ <p class="atlas-meta">對外能力:訪客憑邀請碼建立帳號(消費邀請碼)。</p>
25
+ </header>
26
+
27
+ <section class="feature-story">
28
+ <h2>使用者故事</h2>
29
+ <p>訪客提交註冊 → 系統以邀請碼狀態判定 → 通過則建立帳號並核銷邀請碼,失敗則不留下任何持久化副作用。</p>
30
+ <p class="atlas-meta">
31
+ <strong>跨子模組交互(誰呼叫誰、誰讀誰寫、回應如何流回)以及與「獲取邀請碼」功能的資料行傳遞,已在<a href="../../index.html">宏觀架構圖</a>內統一呈現</strong>,本頁不重述。
32
+ </p>
33
+ </section>
34
+
35
+ <section class="submodule-nav">
36
+ <h2>本功能的子模組</h2>
37
+ <table>
38
+ <thead><tr><th>子模組</th><th>自身職責</th><th>頁面(自身 I/O + 內部資料流)</th></tr></thead>
39
+ <tbody>
40
+ <tr><td><code>web-register-ui</code></td><td>表單、客戶端驗證、發 HTTP</td><td><a href="web-register-ui.html">web-register-ui.html</a></td></tr>
41
+ <tr><td><code>public-api</code></td><td>HTTP 邊界、輸入綁定、錯誤映射</td><td><a href="public-api.html">public-api.html</a></td></tr>
42
+ <tr><td><code>registration-service</code></td><td>網域規則、交易編排</td><td><a href="registration-service.html">registration-service.html</a></td></tr>
43
+ <tr><td><code>postgresql</code></td><td>SQL:讀邀請碼、寫 user、核銷邀請</td><td><a href="postgresql.html">postgresql.html</a></td></tr>
44
+ </tbody>
45
+ </table>
46
+ </section>
47
+
48
+ <footer class="atlas-meta" style="margin-top: 2rem">
49
+ <p><a href="../../index.html">← 宏觀架構(含完整交互圖)</a> · <code>features/invite-code-registration/</code></p>
50
+ </footer>
51
+ </main>
52
+ </body>
53
+ </html>
@@ -0,0 +1,161 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-Hant">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>子模組 · postgresql — 邀請碼註冊(reg 視角)</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;600&family=Plus+Jakarta+Sans:wght@400;500;600&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="../../assets/architecture.css" />
14
+ </head>
15
+ <body>
16
+ <main class="atlas-page atlas-page--feature">
17
+ <nav class="breadcrumb" aria-label="階層導航">
18
+ <a href="../../index.html">宏觀架構</a> · <a href="./index.html">邀請碼註冊</a> · <span>postgresql</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Submodule · postgresql(reg 視角)</p>
23
+ <h1 class="atlas-title">postgresql — 註冊側操作</h1>
24
+ <p class="atlas-meta">
25
+ 自身職責:以 SQL 對 <code>invite_codes</code>/<code>users</code> 表執行讀鎖、寫入與條件更新。本子模組同時是<strong>讀者</strong>(查邀請碼)與<strong>寫者</strong>(建 user、核銷邀請)。本頁只列自身 SQL 函式的 I/O。
26
+ </p>
27
+ </header>
28
+
29
+ <section class="sub-io">
30
+ <h2>SQL 函式 I/O</h2>
31
+ <table>
32
+ <thead><tr><th>函式</th><th>簽名/SQL</th><th>副作用</th><th>用途</th></tr></thead>
33
+ <tbody>
34
+ <tr>
35
+ <td><code>GetInviteForUpdate</code></td>
36
+ <td class="sub-io__signature">
37
+ <strong>in:</strong> <code>tx</code>, <code>code: text</code><br>
38
+ <strong>out:</strong> <code>InviteRow</code> | <code>NotFound</code><br>
39
+ SQL: <code>SELECT … FROM invite_codes WHERE code=$1 FOR UPDATE</code>
40
+ </td>
41
+ <td><span class="sub-io__side sub-io__side--io">lock</span> 對該列加 row lock 直到 tx 結束</td>
42
+ <td>取得邀請碼狀態並阻止他人並行核銷。</td>
43
+ </tr>
44
+ <tr>
45
+ <td><code>InsertUser</code></td>
46
+ <td class="sub-io__signature">
47
+ <strong>in:</strong> <code>tx</code>, <code>{email, passwordHash, inviteID}</code><br>
48
+ <strong>out:</strong> <code>UserID</code> | <code>UniqueViolation</code><br>
49
+ SQL: <code>INSERT INTO users (...) RETURNING id</code>
50
+ </td>
51
+ <td><span class="sub-io__side sub-io__side--write">write</span> 新列(待 commit)</td>
52
+ <td>建立使用者列;<code>users.email UNIQUE</code>。</td>
53
+ </tr>
54
+ <tr>
55
+ <td><code>MarkInviteConsumed</code></td>
56
+ <td class="sub-io__signature">
57
+ <strong>in:</strong> <code>tx</code>, <code>inviteID</code>, <code>userID</code>, <code>now</code><br>
58
+ <strong>out:</strong> <code>RowsAffected</code><br>
59
+ SQL: <code>UPDATE invite_codes SET consumed_at=$3, consumed_by=$2 WHERE id=$1 AND consumed_at IS NULL</code>
60
+ </td>
61
+ <td><span class="sub-io__side sub-io__side--write">write</span> 條件更新</td>
62
+ <td>標記核銷;<code>RowsAffected=0</code> 表示已被搶先核銷。</td>
63
+ </tr>
64
+ </tbody>
65
+ </table>
66
+ </section>
67
+
68
+ <section class="sub-vars">
69
+ <h2>變數與業務用途</h2>
70
+ <p class="sub-vars__intro">本表分兩段:上半為 SQL 函式參數,下半為 <code>invite_codes</code> / <code>users</code> 表上的關鍵列欄位(被本子模組讀寫)。</p>
71
+ <table>
72
+ <thead><tr><th>變數</th><th>型別</th><th>作用域</th><th>業務用途</th></tr></thead>
73
+ <tbody>
74
+ <tr>
75
+ <td class="sub-vars__name">tx</td>
76
+ <td class="sub-vars__type">*sql.Tx</td>
77
+ <td><span class="sub-vars__scope sub-vars__scope--tx">tx</span></td>
78
+ <td>上層管理的隔離單元;本子模組所有 SQL 都掛在這個 <code>tx</code> 上,未 commit 即視為「無事發生」,保證註冊原子性。</td>
79
+ </tr>
80
+ <tr>
81
+ <td class="sub-vars__name">code</td>
82
+ <td class="sub-vars__type">text</td>
83
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
84
+ <td><code>GetInviteForUpdate</code> 的查鎖鍵;對應到列上欄位 <code>invite_codes.code</code> 的 UNIQUE 索引,避免並行核銷相同邀請。</td>
85
+ </tr>
86
+ <tr>
87
+ <td class="sub-vars__name">inviteID</td>
88
+ <td class="sub-vars__type">UUID</td>
89
+ <td><span class="sub-vars__scope sub-vars__scope--tx">tx</span></td>
90
+ <td>從 SELECT 取得後跨多個寫入;把「核銷哪一張邀請」與「新 user 屬於哪張邀請」綁定,作為日後審計鏈。</td>
91
+ </tr>
92
+ <tr>
93
+ <td class="sub-vars__name">userID</td>
94
+ <td class="sub-vars__type">UUID</td>
95
+ <td><span class="sub-vars__scope sub-vars__scope--tx">tx</span></td>
96
+ <td><code>InsertUser RETURNING id</code> 的結果;同一 <code>tx</code> 內回填到 <code>consumed_by</code>,完成「誰用了邀請」的紀錄。</td>
97
+ </tr>
98
+ <tr>
99
+ <td class="sub-vars__name">now</td>
100
+ <td class="sub-vars__type">timestamp</td>
101
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
102
+ <td>核銷時的官方時間;由應用層提供以避免應用與 DB 時鐘漂移影響審計順序。</td>
103
+ </tr>
104
+ <tr>
105
+ <td class="sub-vars__name">RowsAffected</td>
106
+ <td class="sub-vars__type">int</td>
107
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
108
+ <td><code>MarkInviteConsumed</code> 條件更新結果;<code>0</code> 表示已被搶先核銷,本次必須回滾,否則會發生雙倍消費。</td>
109
+ </tr>
110
+ <tr>
111
+ <td class="sub-vars__name">consumed_at</td>
112
+ <td class="sub-vars__type">timestamp NULL</td>
113
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
114
+ <td><code>invite_codes</code> 列上欄位;非空代表「這張邀請已被消費」,與 <code>WHERE consumed_at IS NULL</code> 一起防雙重核銷。</td>
115
+ </tr>
116
+ <tr>
117
+ <td class="sub-vars__name">consumed_by</td>
118
+ <td class="sub-vars__type">FK → users.id</td>
119
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
120
+ <td><code>invite_codes</code> 列上欄位;紀錄是哪個 user 用掉這張邀請,是「邀請碼註冊」這條業務路徑留下的最終痕跡。</td>
121
+ </tr>
122
+ <tr>
123
+ <td class="sub-vars__name">users.email</td>
124
+ <td class="sub-vars__type">text UNIQUE</td>
125
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
126
+ <td>使用者唯一鍵;UNIQUE 違例代表「相同信箱已註冊」,業務上以 409 回應,呼叫端不應靜默忽略。</td>
127
+ </tr>
128
+ </tbody>
129
+ </table>
130
+ </section>
131
+
132
+ <section class="sub-dataflow">
133
+ <h2>內部資料流(單一交易視角)</h2>
134
+ <ol>
135
+ <li>呼叫端開 <code>tx</code> → 本子模組以該 <code>tx</code> 執行 SQL;交易控制不在本子模組內。</li>
136
+ <li><code>GetInviteForUpdate</code> 取得 <code>InviteRow</code> 並把 <strong>列鎖</strong> 綁在 <code>tx</code> 上。</li>
137
+ <li><code>InsertUser</code> 取得 <code>UserID</code>,作為下一步 <code>consumed_by</code> 的 FK 值。</li>
138
+ <li><code>MarkInviteConsumed</code> 以 <code>WHERE consumed_at IS NULL</code> 防雙重核銷。</li>
139
+ <li>所有寫入皆在 <code>tx</code> 內;commit/rollback 由呼叫端決定,本子模組對外只報告每個 SQL 的結果。</li>
140
+ </ol>
141
+ </section>
142
+
143
+ <section class="sub-errors">
144
+ <h2>本子模組會回傳的錯誤</h2>
145
+ <table>
146
+ <thead><tr><th>錯誤</th><th>觸發 SQL</th><th>含義</th></tr></thead>
147
+ <tbody>
148
+ <tr><td><code>NotFound</code></td><td><code>GetInviteForUpdate</code></td><td>無該邀請碼列。</td></tr>
149
+ <tr><td><code>UniqueViolation</code></td><td><code>InsertUser</code></td><td>email 重複。</td></tr>
150
+ <tr><td><code>LockWaitTimeout</code></td><td><code>GetInviteForUpdate</code></td><td>其他並發交易長時間持有鎖。</td></tr>
151
+ <tr><td><code>ConnTransient</code></td><td>任意</td><td>網路/restart;交易不會 partial。</td></tr>
152
+ </tbody>
153
+ </table>
154
+ </section>
155
+
156
+ <footer class="atlas-meta" style="margin-top: 2rem">
157
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
158
+ </footer>
159
+ </main>
160
+ </body>
161
+ </html>