@laitszkin/apollo-toolkit 3.9.6 → 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 (46) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +23 -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-changes-from-local-branches/SKILL.md +75 -128
  36. package/merge-changes-from-local-branches/agents/openai.yaml +1 -1
  37. package/merge-conflict-resolver/agents/openai.yaml +5 -0
  38. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  39. package/package.json +1 -1
  40. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  41. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  42. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  43. package/spec-to-project-html/SKILL.md +116 -0
  44. package/spec-to-project-html/agents/openai.yaml +12 -0
  45. package/spec-to-project-html/references/TEMPLATE_SPEC.md +98 -0
  46. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
@@ -0,0 +1,54 @@
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 · get-invite-codes</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>使用者進入「我的邀請碼」頁面 → 按生成 → 系統檢查額度與帳號狀態 → 產生密碼學隨機字串並寫入 <code>invite_codes</code>。</p>
30
+ <p class="atlas-meta">
31
+ <strong>跨子模組交互</strong>(含 generator 的多次重試呼叫、DB UNIQUE 衝突回傳、跨功能 <code>invite_codes</code> 行被「邀請碼註冊」消費)已在<a href="../../index.html">宏觀架構圖</a>內統一呈現,本頁不重述。
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-get-invite-ui</code></td><td>登入後頁面、列表、生成按鈕</td><td><a href="web-get-invite-ui.html">web-get-invite-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>invite-issuance-service</code></td><td>額度、交易、碰撞重試策略</td><td><a href="invite-issuance-service.html">invite-issuance-service.html</a></td></tr>
43
+ <tr><td><code>invite-code-generator</code></td><td>純函式:熵 → 字串</td><td><a href="invite-code-generator.html">invite-code-generator.html</a></td></tr>
44
+ <tr><td><code>postgresql</code></td><td>SQL:計數、INSERT 新邀請碼列、讀帳號狀態</td><td><a href="postgresql.html">postgresql.html</a></td></tr>
45
+ </tbody>
46
+ </table>
47
+ </section>
48
+
49
+ <footer class="atlas-meta" style="margin-top: 2rem">
50
+ <p><a href="../../index.html">← 宏觀架構(含完整交互圖)</a> · <code>features/get-invite-codes/</code></p>
51
+ </footer>
52
+ </main>
53
+ </body>
54
+ </html>
@@ -0,0 +1,165 @@
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>子模組 · invite-code-generator — 獲取邀請碼</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>invite-code-generator</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Submodule · invite-code-generator</p>
23
+ <h1 class="atlas-title">invite-code-generator</h1>
24
+ <p class="atlas-meta">自身職責:把熵編碼成符合產品規格的可讀字串。<strong>無 I/O 副作用</strong>(除讀取隨機源)。本頁只列自身函式 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>NewGenerator</code></td>
34
+ <td class="sub-io__signature">
35
+ <strong>in:</strong> <code>Config{bits, alphabet, groupSize}</code><br>
36
+ <strong>out:</strong> <code>Generator</code>
37
+ </td>
38
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
39
+ <td>建構,驗證 <code>bits &gt;= 64</code>、字元表非空且不含易混字。</td>
40
+ </tr>
41
+ <tr>
42
+ <td><code>Generator.Next</code></td>
43
+ <td class="sub-io__signature">
44
+ <strong>in:</strong> —<br>
45
+ <strong>out:</strong> <code>string</code> | <code>ErrEntropy</code>
46
+ </td>
47
+ <td><span class="sub-io__side sub-io__side--io">io</span> 讀 <code>crypto/rand</code></td>
48
+ <td>單次抽碼。</td>
49
+ </tr>
50
+ <tr>
51
+ <td><code>encode</code></td>
52
+ <td class="sub-io__signature">
53
+ <strong>in:</strong> <code>bytes</code>, <code>alphabet</code><br>
54
+ <strong>out:</strong> <code>string</code>
55
+ </td>
56
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
57
+ <td>把位元組以 Crockford Base32(或同類)映射成字元。</td>
58
+ </tr>
59
+ <tr>
60
+ <td><code>group</code></td>
61
+ <td class="sub-io__signature">
62
+ <strong>in:</strong> <code>string</code>, <code>groupSize</code><br>
63
+ <strong>out:</strong> <code>string</code>
64
+ </td>
65
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
66
+ <td>每 N 位插入 <code>-</code>,便於人讀。</td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ </section>
71
+
72
+ <section class="sub-vars">
73
+ <h2>變數與業務用途</h2>
74
+ <p class="sub-vars__intro">本子模組屬於純函式工具;下表分兩段:上半為建構時固定的設定,下半為單次 <code>Next</code> 期間的臨時值。</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">Config</td>
80
+ <td class="sub-vars__type">{bits, alphabet, groupSize}</td>
81
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
82
+ <td>整個 Generator 生命週期固定;決定碼強度(bits)、可讀性(alphabet)、人讀分組(groupSize)。</td>
83
+ </tr>
84
+ <tr>
85
+ <td class="sub-vars__name">bits</td>
86
+ <td class="sub-vars__type">int</td>
87
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
88
+ <td>熵預算;決定隨機字串對 UNIQUE 碰撞的容忍度;過低會推高呼叫端的重試次數與失敗率。</td>
89
+ </tr>
90
+ <tr>
91
+ <td class="sub-vars__name">alphabet</td>
92
+ <td class="sub-vars__type">string</td>
93
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
94
+ <td>字元表;剃除 0/O、1/I/l 等易混字,業務上降低使用者抄錄錯誤率,間接提高分享成功率。</td>
95
+ </tr>
96
+ <tr>
97
+ <td class="sub-vars__name">groupSize</td>
98
+ <td class="sub-vars__type">int</td>
99
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
100
+ <td>每幾個字元插入連字號;只影響顯示/輸入體驗,不影響熵或唯一性。</td>
101
+ </tr>
102
+ <tr>
103
+ <td class="sub-vars__name">bytes / buf</td>
104
+ <td class="sub-vars__type">[]byte</td>
105
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
106
+ <td>從 <code>crypto/rand</code> 讀出的原始熵;只在單次 <code>Next</code> 期間活著,不持久化、不外傳,避免被推測下一次值。</td>
107
+ </tr>
108
+ <tr>
109
+ <td class="sub-vars__name">raw</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>經 base32 編碼後尚未分組的中介字串;只是內部步驟,不對外。</td>
113
+ </tr>
114
+ <tr>
115
+ <td class="sub-vars__name">out / candidate</td>
116
+ <td class="sub-vars__type">string</td>
117
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
118
+ <td>最終回傳的候選碼;本子模組不快取、不記錄;唯一性由呼叫端結合 DB UNIQUE 保證。</td>
119
+ </tr>
120
+ </tbody>
121
+ </table>
122
+ </section>
123
+
124
+ <section class="sub-dataflow">
125
+ <h2>內部資料流(單次 Next)</h2>
126
+ <ol>
127
+ <li>從 <code>crypto/rand</code> 讀 <code>bytes[ceil(bits/8)]</code>。</li>
128
+ <li><code>bytes</code> → <code>encode(bytes, alphabet)</code> → <code>raw: string</code>。</li>
129
+ <li><code>raw</code> → <code>group(raw, groupSize)</code> → 最終字串。</li>
130
+ <li>內部不持有狀態(除設定);多次呼叫互相獨立,無快取無記錄。</li>
131
+ </ol>
132
+ <svg class="sub-dataflow__svg" viewBox="0 0 720 110" role="img" aria-label="生成器資料流">
133
+ <defs>
134
+ <marker id="d-arr3" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
135
+ <path d="M0,0 L9,4.5 L0,9 Z" fill="currentColor"></path>
136
+ </marker>
137
+ </defs>
138
+ <g class="d-node"><rect x="20" y="30" width="150" height="50" rx="8"></rect><text x="95" y="50" text-anchor="middle">crypto/rand</text><text x="95" y="68" text-anchor="middle" class="d-label">→ bytes</text></g>
139
+ <g class="d-node"><rect x="220" y="30" width="150" height="50" rx="8"></rect><text x="295" y="50" text-anchor="middle">encode</text><text x="295" y="68" text-anchor="middle" class="d-label">bytes → raw</text></g>
140
+ <g class="d-node"><rect x="420" y="30" width="150" height="50" rx="8"></rect><text x="495" y="50" text-anchor="middle">group</text><text x="495" y="68" text-anchor="middle" class="d-label">raw → 字串</text></g>
141
+ <g class="d-node"><rect x="600" y="30" width="100" height="50" rx="8"></rect><text x="650" y="58" text-anchor="middle">candidate</text></g>
142
+ <line class="d-edge" x1="170" y1="55" x2="216" y2="55" marker-end="url(#d-arr3)"></line>
143
+ <line class="d-edge" x1="370" y1="55" x2="416" y2="55" marker-end="url(#d-arr3)"></line>
144
+ <line class="d-edge" x1="570" y1="55" x2="596" y2="55" marker-end="url(#d-arr3)"></line>
145
+ </svg>
146
+ </section>
147
+
148
+ <section class="sub-errors">
149
+ <h2>本子模組會拋出的錯誤</h2>
150
+ <table>
151
+ <thead><tr><th>錯誤</th><th>條件</th></tr></thead>
152
+ <tbody>
153
+ <tr><td><code>ErrEntropy</code></td><td><code>crypto/rand.Read</code> 短讀/失敗(罕見)。</td></tr>
154
+ <tr><td><code>ErrConfig</code></td><td><code>NewGenerator</code> 收到非法 <code>Config</code>。</td></tr>
155
+ </tbody>
156
+ </table>
157
+ <p class="atlas-meta">注意:「唯一性」<strong>不在本子模組</strong>;DB UNIQUE 違例由呼叫端處理(重新呼叫 <code>Next</code>)。</p>
158
+ </section>
159
+
160
+ <footer class="atlas-meta" style="margin-top: 2rem">
161
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
162
+ </footer>
163
+ </main>
164
+ </body>
165
+ </html>
@@ -0,0 +1,198 @@
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>子模組 · invite-issuance-service — 獲取邀請碼</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>invite-issuance-service</span>
19
+ </nav>
20
+
21
+ <header class="atlas-header">
22
+ <p class="atlas-kicker">Submodule · invite-issuance-service</p>
23
+ <h1 class="atlas-title">invite-issuance-service</h1>
24
+ <p class="atlas-meta">自身職責:在同一交易內判斷使用者額度、組裝待寫入列、處理 UNIQUE 碰撞重試。本頁只列自身函式 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>IssueInviteCode</code></td>
34
+ <td class="sub-io__signature">
35
+ <strong>in:</strong> <code>ctx</code>, <code>userID</code><br>
36
+ <strong>out:</strong> <code>InviteCodeRecord</code> | <code>error</code>
37
+ </td>
38
+ <td><span class="sub-io__side sub-io__side--io">tx</span> 開/關交易<br><span class="sub-io__side sub-io__side--write">db</span> 條件成立時寫入新行</td>
39
+ <td>對外唯一入口。</td>
40
+ </tr>
41
+ <tr>
42
+ <td><code>checkQuota</code></td>
43
+ <td class="sub-io__signature">
44
+ <strong>in:</strong> <code>used</code>, <code>limit</code>, <code>userStatus</code><br>
45
+ <strong>out:</strong> <code>true | ErrQuotaExceeded | ErrForbidden</code>
46
+ </td>
47
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
48
+ <td>窗口計數 + 帳號狀態合法性判斷。</td>
49
+ </tr>
50
+ <tr>
51
+ <td><code>buildRecord</code></td>
52
+ <td class="sub-io__signature">
53
+ <strong>in:</strong> <code>userID</code>, <code>code: string</code>, <code>now</code><br>
54
+ <strong>out:</strong> <code>InviteCodeRecord</code>
55
+ </td>
56
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
57
+ <td>組裝待寫入承載:<code>owner_user_id</code>、<code>allowed_registration=true</code>、<code>created_at=now</code>。</td>
58
+ </tr>
59
+ <tr>
60
+ <td><code>shouldRetryOnCollision</code></td>
61
+ <td class="sub-io__signature">
62
+ <strong>in:</strong> <code>err</code>, <code>attempt: int</code>, <code>max: int</code><br>
63
+ <strong>out:</strong> <code>bool</code>
64
+ </td>
65
+ <td><span class="sub-io__side sub-io__side--pure">pure</span></td>
66
+ <td><code>err</code> 為 UNIQUE 違例且 <code>attempt &lt; max</code> 才回 <code>true</code>。</td>
67
+ </tr>
68
+ </tbody>
69
+ </table>
70
+ </section>
71
+
72
+ <section class="sub-vars">
73
+ <h2>變數與業務用途</h2>
74
+ <p class="sub-vars__intro">本表盤點額度判定、寫入承載組裝、與 UNIQUE 碰撞重試所依賴的識別子。</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">ctx</td>
80
+ <td class="sub-vars__type">context.Context</td>
81
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
82
+ <td>限制整次發放的時間預算;避免重試迴圈無限延長導致連線資源被吃光。</td>
83
+ </tr>
84
+ <tr>
85
+ <td class="sub-vars__name">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>本次發放歸屬;同時也是額度視窗的主鍵;由 <code>public-api</code> 從可信來源(Principal)注入。</td>
89
+ </tr>
90
+ <tr>
91
+ <td class="sub-vars__name">tx</td>
92
+ <td class="sub-vars__type">*sql.Tx</td>
93
+ <td><span class="sub-vars__scope sub-vars__scope--tx">tx</span></td>
94
+ <td>把「額度檢查 + 帳號狀態檢查 + 寫入」綁成原子單元;避免在檢查與寫入之間出現空隙造成雙倍超發。</td>
95
+ </tr>
96
+ <tr>
97
+ <td class="sub-vars__name">used</td>
98
+ <td class="sub-vars__type">int64</td>
99
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
100
+ <td>當前滑動視窗已發放數;額度判定的「左值」;錯算即可能放行超額或誤拒。</td>
101
+ </tr>
102
+ <tr>
103
+ <td class="sub-vars__name">limit</td>
104
+ <td class="sub-vars__type">int</td>
105
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
106
+ <td>滑動視窗額度上限;產品設定,跨呼叫不變;改值代表產品策略改變,需配套更新文案。</td>
107
+ </tr>
108
+ <tr>
109
+ <td class="sub-vars__name">userStatus</td>
110
+ <td class="sub-vars__type">active | banned | suspended</td>
111
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
112
+ <td>帳號是否處於可發放狀態;停權或暫停狀態下立即拒絕,不浪費 generator/DB 資源。</td>
113
+ </tr>
114
+ <tr>
115
+ <td class="sub-vars__name">now</td>
116
+ <td class="sub-vars__type">time.Time</td>
117
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
118
+ <td>滑動視窗起點計算與 <code>created_at</code> 的基準;測試可注入。</td>
119
+ </tr>
120
+ <tr>
121
+ <td class="sub-vars__name">attempt</td>
122
+ <td class="sub-vars__type">int</td>
123
+ <td><span class="sub-vars__scope sub-vars__scope--loop">loop</span></td>
124
+ <td>UNIQUE 碰撞重試計數;搭配 <code>max</code> 限制最壞情況避免無限循環。</td>
125
+ </tr>
126
+ <tr>
127
+ <td class="sub-vars__name">max</td>
128
+ <td class="sub-vars__type">int</td>
129
+ <td><span class="sub-vars__scope sub-vars__scope--instance">instance</span></td>
130
+ <td>重試上限;超出即拋 <code>ErrCollisionExhausted</code>;上限與 generator 的熵預算共同決定罕見失敗率。</td>
131
+ </tr>
132
+ <tr>
133
+ <td class="sub-vars__name">candidate</td>
134
+ <td class="sub-vars__type">string</td>
135
+ <td><span class="sub-vars__scope sub-vars__scope--loop">loop</span></td>
136
+ <td>一輪重試取得的候選碼;尚未確定唯一;UNIQUE 違例即丟棄並重抽,不會被使用者看到。</td>
137
+ </tr>
138
+ <tr>
139
+ <td class="sub-vars__name">InviteCodeRecord</td>
140
+ <td class="sub-vars__type">{code, owner_user_id, allowed_registration, created_at}</td>
141
+ <td><span class="sub-vars__scope sub-vars__scope--loop">loop</span></td>
142
+ <td>寫入 DB 的承載;<code>allowed_registration=true</code> 是「邀請碼註冊」功能消費的關鍵旗標——關閉此值代表暫停發放但保留歷史紀錄。</td>
143
+ </tr>
144
+ </tbody>
145
+ </table>
146
+ </section>
147
+
148
+ <section class="sub-dataflow">
149
+ <h2>內部資料流</h2>
150
+ <ol>
151
+ <li>取得 <code>userID</code> + 開 <code>tx</code> → 讀<code>{used, userStatus}</code>(透過 DB 子模組;其 SQL 介面見宏觀圖邊 <code>e-B-...</code>)。</li>
152
+ <li><code>checkQuota</code> 不通過 → 拋 <code>ErrQuotaExceeded</code>/<code>ErrForbidden</code>;本函式直接結束(呼叫端負責回滾)。</li>
153
+ <li><code>candidate</code> ← 生成器產出的字串。</li>
154
+ <li><code>buildRecord(userID, candidate, now)</code> → <code>InviteCodeRecord</code>。</li>
155
+ <li>嘗試寫入;若 <code>err</code> 與 <code>shouldRetryOnCollision(err, attempt, max)</code> 為 <code>true</code>,<code>attempt+=1</code>,回到步驟 3。</li>
156
+ <li>寫入成功 → 提交 → 回傳 <code>InviteCodeRecord</code>。</li>
157
+ </ol>
158
+ <svg class="sub-dataflow__svg" viewBox="0 0 720 220" role="img" aria-label="內部資料流">
159
+ <defs>
160
+ <marker id="d-arr2" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
161
+ <path d="M0,0 L9,4.5 L0,9 Z" fill="currentColor"></path>
162
+ </marker>
163
+ </defs>
164
+ <g class="d-node"><rect x="20" y="30" width="150" height="50" rx="8"></rect><text x="95" y="58" text-anchor="middle">userID</text></g>
165
+ <g class="d-node"><rect x="200" y="30" width="150" height="50" rx="8"></rect><text x="275" y="50" text-anchor="middle">checkQuota</text><text x="275" y="68" text-anchor="middle" class="d-label">→ true | err</text></g>
166
+ <g class="d-node"><rect x="380" y="30" width="170" height="50" rx="8"></rect><text x="465" y="50" text-anchor="middle">candidate</text><text x="465" y="68" text-anchor="middle" class="d-label">← Generator.Next()</text></g>
167
+ <g class="d-node"><rect x="580" y="30" width="120" height="50" rx="8"></rect><text x="640" y="58" text-anchor="middle">buildRecord</text></g>
168
+
169
+ <g class="d-node"><rect x="380" y="140" width="170" height="50" rx="8"></rect><text x="465" y="160" text-anchor="middle">shouldRetryOnCollision</text><text x="465" y="178" text-anchor="middle" class="d-label">err+attempt → bool</text></g>
170
+ <g class="d-node"><rect x="580" y="140" width="120" height="50" rx="8"></rect><text x="640" y="168" text-anchor="middle">InviteCodeRecord</text></g>
171
+
172
+ <line class="d-edge" x1="170" y1="55" x2="196" y2="55" marker-end="url(#d-arr2)"></line>
173
+ <line class="d-edge" x1="350" y1="55" x2="376" y2="55" marker-end="url(#d-arr2)"></line>
174
+ <line class="d-edge" x1="550" y1="55" x2="576" y2="55" marker-end="url(#d-arr2)"></line>
175
+ <line class="d-edge" x1="640" y1="80" x2="640" y2="136" marker-end="url(#d-arr2)"></line>
176
+ <line class="d-edge d-edge--side" x1="465" y1="190" x2="465" y2="80" marker-end="url(#d-arr2)"></line>
177
+ </svg>
178
+ </section>
179
+
180
+ <section class="sub-errors">
181
+ <h2>本子模組會拋出的錯誤</h2>
182
+ <table>
183
+ <thead><tr><th>錯誤</th><th>條件</th></tr></thead>
184
+ <tbody>
185
+ <tr><td><code>ErrQuotaExceeded</code></td><td><code>checkQuota</code> 失敗(含 <code>retryAfter</code>)</td></tr>
186
+ <tr><td><code>ErrForbidden</code></td><td>帳號停用</td></tr>
187
+ <tr><td><code>ErrCollisionExhausted</code></td><td>重試次數用盡;極罕見,視熵設計。</td></tr>
188
+ <tr><td><code>ErrTransient</code></td><td>連線中斷/<code>ctx</code> 取消</td></tr>
189
+ </tbody>
190
+ </table>
191
+ </section>
192
+
193
+ <footer class="atlas-meta" style="margin-top: 2rem">
194
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
195
+ </footer>
196
+ </main>
197
+ </body>
198
+ </html>
@@ -0,0 +1,165 @@
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 — 獲取邀請碼(iss 視角)</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(iss 視角)</p>
23
+ <h1 class="atlas-title">postgresql — 發放側操作</h1>
24
+ <p class="atlas-meta">
25
+ 自身職責:對 <code>invite_codes</code> 表執行<strong>計數</strong>與<strong>插入</strong>。寫入的列會在「邀請碼註冊」功能裡被另一個視角讀取(跨功能資料流見<a href="../../index.html">宏觀架構圖</a>)。本頁只列自身 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>CountInvitesSince</code></td>
36
+ <td class="sub-io__signature">
37
+ <strong>in:</strong> <code>tx</code>, <code>userID</code>, <code>since</code><br>
38
+ <strong>out:</strong> <code>int64</code><br>
39
+ SQL: <code>SELECT count(*) FROM invite_codes WHERE owner_user_id=$1 AND created_at &gt; $2</code>
40
+ </td>
41
+ <td><span class="sub-io__side sub-io__side--io">read</span></td>
42
+ <td>滑動視窗計數,給呼叫端做額度判斷。</td>
43
+ </tr>
44
+ <tr>
45
+ <td><code>InsertInviteCode</code></td>
46
+ <td class="sub-io__signature">
47
+ <strong>in:</strong> <code>tx</code>, <code>InviteCodeRecord</code><br>
48
+ <strong>out:</strong> <code>InviteID</code> | <code>UniqueViolation</code><br>
49
+ SQL: <code>INSERT INTO invite_codes (code, owner_user_id, allowed_registration, ...) VALUES (...) RETURNING id</code>
50
+ </td>
51
+ <td><span class="sub-io__side sub-io__side--write">write</span> 新列(待 commit)</td>
52
+ <td><code>invite_codes.code UNIQUE</code>;衝突時呼叫端負責換碼重試。</td>
53
+ </tr>
54
+ <tr>
55
+ <td><code>GetUserStatus</code></td>
56
+ <td class="sub-io__signature">
57
+ <strong>in:</strong> <code>tx</code>, <code>userID</code><br>
58
+ <strong>out:</strong> <code>UserStatus</code> | <code>NotFound</code><br>
59
+ SQL: <code>SELECT status, banned_at FROM users WHERE id=$1</code>
60
+ </td>
61
+ <td><span class="sub-io__side sub-io__side--io">read</span></td>
62
+ <td>給呼叫端做帳號狀態檢查。</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> 上以保證原子寫入。</td>
79
+ </tr>
80
+ <tr>
81
+ <td class="sub-vars__name">userID</td>
82
+ <td class="sub-vars__type">UUID</td>
83
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
84
+ <td>SQL 參數;標記新邀請碼擁有者,也是計數視窗鍵;錯誤帶入會把額度算到別人頭上。</td>
85
+ </tr>
86
+ <tr>
87
+ <td class="sub-vars__name">since</td>
88
+ <td class="sub-vars__type">timestamp</td>
89
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
90
+ <td>滑動視窗起點;決定 <code>count(*)</code> 的時段;起點漂移會讓額度執行不準。</td>
91
+ </tr>
92
+ <tr>
93
+ <td class="sub-vars__name">InviteCodeRecord</td>
94
+ <td class="sub-vars__type">{code, owner_user_id, allowed_registration, ...}</td>
95
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
96
+ <td>待插入的邀請碼資料;其欄位直接落在 <code>invite_codes</code> 列上。</td>
97
+ </tr>
98
+ <tr>
99
+ <td class="sub-vars__name">UserStatus</td>
100
+ <td class="sub-vars__type">{status, banned_at}</td>
101
+ <td><span class="sub-vars__scope sub-vars__scope--call">call</span></td>
102
+ <td><code>GetUserStatus</code> 的回傳;給呼叫端判定是否准予發放。</td>
103
+ </tr>
104
+ <tr>
105
+ <td class="sub-vars__name">invite_codes.code</td>
106
+ <td class="sub-vars__type">text UNIQUE</td>
107
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
108
+ <td>列上欄位;UNIQUE 約束在本子模組是「衝突信號源」,呼叫端據此換碼重試。</td>
109
+ </tr>
110
+ <tr>
111
+ <td class="sub-vars__name">invite_codes.owner_user_id</td>
112
+ <td class="sub-vars__type">FK → users.id</td>
113
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
114
+ <td>列上欄位;建立「誰擁有這張邀請」的關聯;額度查詢與後續審計都依此欄。</td>
115
+ </tr>
116
+ <tr>
117
+ <td class="sub-vars__name">invite_codes.allowed_registration</td>
118
+ <td class="sub-vars__type">boolean</td>
119
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
120
+ <td>列上欄位;發放時寫 <code>true</code>;「邀請碼註冊」功能在判定時讀此欄,<code>false</code> 即拒絕該邀請可用。</td>
121
+ </tr>
122
+ <tr>
123
+ <td class="sub-vars__name">invite_codes.consumed_at</td>
124
+ <td class="sub-vars__type">timestamp NULL</td>
125
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
126
+ <td>列上欄位;本子模組寫入時為 <code>NULL</code>;之後由「邀請碼註冊」標記消費,是橋接兩個功能模組的關鍵欄位。</td>
127
+ </tr>
128
+ <tr>
129
+ <td class="sub-vars__name">invite_codes.created_at</td>
130
+ <td class="sub-vars__type">timestamp</td>
131
+ <td><span class="sub-vars__scope sub-vars__scope--persist">persist</span></td>
132
+ <td>列上欄位;額度的滑動視窗計數憑此欄位,業務上要與 <code>now</code> 同步以免漂移。</td>
133
+ </tr>
134
+ </tbody>
135
+ </table>
136
+ </section>
137
+
138
+ <section class="sub-dataflow">
139
+ <h2>內部資料流(單一交易視角)</h2>
140
+ <ol>
141
+ <li>呼叫端開 <code>tx</code> → 本子模組以該 <code>tx</code> 執行 SQL;交易控制不在本子模組內。</li>
142
+ <li><code>CountInvitesSince</code> 與 <code>GetUserStatus</code> 為純讀;不變動列。</li>
143
+ <li><code>InsertInviteCode</code>:寫入後回傳 <code>InviteID</code> 或 <code>UniqueViolation</code>;<strong>無</strong> partial row。</li>
144
+ <li>所有寫入皆在 <code>tx</code> 內;本子模組對外只報告每個 SQL 結果。</li>
145
+ </ol>
146
+ </section>
147
+
148
+ <section class="sub-errors">
149
+ <h2>本子模組會回傳的錯誤</h2>
150
+ <table>
151
+ <thead><tr><th>錯誤</th><th>觸發 SQL</th></tr></thead>
152
+ <tbody>
153
+ <tr><td><code>UniqueViolation</code></td><td><code>InsertInviteCode</code>(<code>code</code> 已存在)</td></tr>
154
+ <tr><td><code>NotFound</code></td><td><code>GetUserStatus</code></td></tr>
155
+ <tr><td><code>ConnTransient</code></td><td>任意</td></tr>
156
+ </tbody>
157
+ </table>
158
+ </section>
159
+
160
+ <footer class="atlas-meta" style="margin-top: 2rem">
161
+ <p><a href="../../index.html">← 宏觀架構</a> · <a href="./index.html">← 功能總覽</a></p>
162
+ </footer>
163
+ </main>
164
+ </body>
165
+ </html>