@sfmc-bds/db-server 0.2.0-beta.1 → 0.2.0-beta.11

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 (163) hide show
  1. package/LICENSE +663 -0
  2. package/dist/domain/bridge.d.ts +57 -23
  3. package/dist/domain/bridge.d.ts.map +1 -1
  4. package/dist/domain/bridge.js +77 -6
  5. package/dist/domain/bridge.js.map +2 -2
  6. package/dist/domain/bridge.test.d.ts +2 -0
  7. package/dist/domain/bridge.test.d.ts.map +1 -0
  8. package/dist/domain/bridge.test.js +87 -0
  9. package/dist/domain/bridge.test.js.map +7 -0
  10. package/dist/domain/economy.d.ts +38 -7
  11. package/dist/domain/economy.d.ts.map +1 -1
  12. package/dist/domain/economy.js.map +2 -2
  13. package/dist/domain/messages-bridge-filter.test.d.ts +5 -0
  14. package/dist/domain/messages-bridge-filter.test.d.ts.map +1 -0
  15. package/dist/domain/messages-bridge-filter.test.js +20 -0
  16. package/dist/domain/messages-bridge-filter.test.js.map +7 -0
  17. package/dist/domain/player-session.d.ts +8 -0
  18. package/dist/domain/player-session.d.ts.map +1 -0
  19. package/dist/domain/player-session.js +16 -0
  20. package/dist/domain/player-session.js.map +7 -0
  21. package/dist/domain/player-session.test.d.ts +2 -0
  22. package/dist/domain/player-session.test.d.ts.map +1 -0
  23. package/dist/domain/player-session.test.js +33 -0
  24. package/dist/domain/player-session.test.js.map +7 -0
  25. package/dist/domain/qq-bind-status.test.d.ts +2 -0
  26. package/dist/domain/qq-bind-status.test.d.ts.map +1 -0
  27. package/dist/domain/qq-bind-status.test.js +211 -0
  28. package/dist/domain/qq-bind-status.test.js.map +7 -0
  29. package/dist/domain/qq-events.d.ts +84 -0
  30. package/dist/domain/qq-events.d.ts.map +1 -0
  31. package/dist/domain/qq-events.js +193 -0
  32. package/dist/domain/qq-events.js.map +7 -0
  33. package/dist/domain/qq-events.test.d.ts +5 -0
  34. package/dist/domain/qq-events.test.d.ts.map +1 -0
  35. package/dist/domain/qq-events.test.js +170 -0
  36. package/dist/domain/qq-events.test.js.map +7 -0
  37. package/dist/domain/qq-join.test.d.ts +2 -0
  38. package/dist/domain/qq-join.test.d.ts.map +1 -0
  39. package/dist/domain/qq-join.test.js +353 -0
  40. package/dist/domain/qq-join.test.js.map +7 -0
  41. package/dist/domain/schema.d.ts +15 -18
  42. package/dist/domain/schema.d.ts.map +1 -1
  43. package/dist/domain/schema.js +104 -453
  44. package/dist/domain/schema.js.map +2 -2
  45. package/dist/domain/system-status.d.ts +75 -0
  46. package/dist/domain/system-status.d.ts.map +1 -0
  47. package/dist/domain/system-status.js +181 -0
  48. package/dist/domain/system-status.js.map +7 -0
  49. package/dist/domain/system-status.test.d.ts +5 -0
  50. package/dist/domain/system-status.test.d.ts.map +1 -0
  51. package/dist/domain/system-status.test.js +19 -0
  52. package/dist/domain/system-status.test.js.map +7 -0
  53. package/dist/domain/transaction.d.ts +5 -4
  54. package/dist/domain/transaction.d.ts.map +1 -1
  55. package/dist/domain/transaction.js.map +2 -2
  56. package/dist/env.d.ts +7 -0
  57. package/dist/env.d.ts.map +1 -1
  58. package/dist/env.js +57 -9
  59. package/dist/env.js.map +2 -2
  60. package/dist/index.d.ts +1 -2
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +128 -38
  63. package/dist/index.js.map +2 -2
  64. package/dist/lib/idempotency-store.d.ts +17 -11
  65. package/dist/lib/idempotency-store.d.ts.map +1 -1
  66. package/dist/lib/idempotency-store.js.map +2 -2
  67. package/dist/lib/idempotency.d.ts +3 -4
  68. package/dist/lib/idempotency.d.ts.map +1 -1
  69. package/dist/lib/idempotency.js.map +2 -2
  70. package/dist/lib/log.d.ts.map +1 -1
  71. package/dist/lib/log.js +3 -6
  72. package/dist/lib/log.js.map +2 -2
  73. package/dist/lib/module-state.d.ts +38 -8
  74. package/dist/lib/module-state.d.ts.map +1 -1
  75. package/dist/lib/module-state.js.map +2 -2
  76. package/dist/lib/order-by.d.ts +8 -6
  77. package/dist/lib/order-by.d.ts.map +1 -1
  78. package/dist/lib/order-by.js.map +2 -2
  79. package/dist/lib/sql-helpers.d.ts +25 -13
  80. package/dist/lib/sql-helpers.d.ts.map +1 -1
  81. package/dist/lib/sql-helpers.js.map +2 -2
  82. package/dist/lib/sqlite.d.ts +14 -5
  83. package/dist/lib/sqlite.d.ts.map +1 -1
  84. package/dist/lib/sqlite.js.map +2 -2
  85. package/dist/manifest-loader.d.ts +5 -2
  86. package/dist/manifest-loader.d.ts.map +1 -1
  87. package/dist/manifest-loader.js +16 -6
  88. package/dist/manifest-loader.js.map +2 -2
  89. package/dist/manifest.d.ts +1 -2
  90. package/dist/manifest.d.ts.map +1 -1
  91. package/dist/manifest.js +13 -8
  92. package/dist/manifest.js.map +2 -2
  93. package/dist/module-auth.d.ts.map +1 -1
  94. package/dist/module-auth.js +2 -6
  95. package/dist/module-auth.js.map +2 -2
  96. package/dist/permission-gate.d.ts.map +1 -1
  97. package/dist/permission-gate.js +8 -2
  98. package/dist/permission-gate.js.map +2 -2
  99. package/dist/project-root.d.ts +6 -0
  100. package/dist/project-root.d.ts.map +1 -0
  101. package/dist/project-root.js +9 -0
  102. package/dist/project-root.js.map +7 -0
  103. package/dist/routes/_shared.d.ts +14 -5
  104. package/dist/routes/_shared.d.ts.map +1 -1
  105. package/dist/routes/_shared.js.map +2 -2
  106. package/dist/routes/config.d.ts +8 -11
  107. package/dist/routes/config.d.ts.map +1 -1
  108. package/dist/routes/config.js +17 -23
  109. package/dist/routes/config.js.map +2 -2
  110. package/dist/routes/db-routes.d.ts +8 -2
  111. package/dist/routes/db-routes.d.ts.map +1 -1
  112. package/dist/routes/db-routes.js +5 -12
  113. package/dist/routes/db-routes.js.map +2 -2
  114. package/dist/routes/messages.d.ts +6 -2
  115. package/dist/routes/messages.d.ts.map +1 -1
  116. package/dist/routes/messages.js +17 -3
  117. package/dist/routes/messages.js.map +2 -2
  118. package/dist/routes/module-config-routes.d.ts +13 -9
  119. package/dist/routes/module-config-routes.d.ts.map +1 -1
  120. package/dist/routes/module-config-routes.js.map +2 -2
  121. package/dist/routes/modules.d.ts +6 -0
  122. package/dist/routes/modules.d.ts.map +1 -1
  123. package/dist/routes/modules.js.map +2 -2
  124. package/dist/routes/qq-bind.d.ts +24 -0
  125. package/dist/routes/qq-bind.d.ts.map +1 -0
  126. package/dist/routes/qq-bind.js +179 -0
  127. package/dist/routes/qq-bind.js.map +7 -0
  128. package/dist/routes/qq-events.d.ts +20 -0
  129. package/dist/routes/qq-events.d.ts.map +1 -0
  130. package/dist/routes/qq-events.js +39 -0
  131. package/dist/routes/qq-events.js.map +7 -0
  132. package/dist/routes/qq-join.d.ts +46 -0
  133. package/dist/routes/qq-join.d.ts.map +1 -0
  134. package/dist/routes/qq-join.js +277 -0
  135. package/dist/routes/qq-join.js.map +7 -0
  136. package/dist/routes/service-routes.d.ts +6 -0
  137. package/dist/routes/service-routes.d.ts.map +1 -1
  138. package/dist/routes/service-routes.js.map +2 -2
  139. package/dist/routes/status.d.ts +19 -0
  140. package/dist/routes/status.d.ts.map +1 -0
  141. package/dist/routes/status.js +74 -0
  142. package/dist/routes/status.js.map +7 -0
  143. package/dist/runtime.test.js +216 -4
  144. package/dist/runtime.test.js.map +2 -2
  145. package/dist/schema-registry.d.ts +17 -16
  146. package/dist/schema-registry.d.ts.map +1 -1
  147. package/dist/schema-registry.js +76 -6
  148. package/dist/schema-registry.js.map +2 -2
  149. package/dist/server.d.ts +19 -0
  150. package/dist/server.d.ts.map +1 -1
  151. package/dist/server.js.map +2 -2
  152. package/dist/service-registry.d.ts +39 -9
  153. package/dist/service-registry.d.ts.map +1 -1
  154. package/dist/service-registry.js +30 -2
  155. package/dist/service-registry.js.map +2 -2
  156. package/dist/services/builtin-handlers.d.ts +27 -7
  157. package/dist/services/builtin-handlers.d.ts.map +1 -1
  158. package/dist/services/builtin-handlers.js.map +2 -2
  159. package/dist/tx-runner.d.ts +48 -9
  160. package/dist/tx-runner.d.ts.map +1 -1
  161. package/dist/tx-runner.js +99 -27
  162. package/dist/tx-runner.js.map +2 -2
  163. package/package.json +26 -24
@@ -0,0 +1,179 @@
1
+ import { randomInt } from "node:crypto";
2
+ import { SQL } from "sql-template-strings";
3
+ const CODE_TTL_MS = 5 * 60 * 1e3;
4
+ const CODE_LEN = 6;
5
+ function genCode() {
6
+ let s = "";
7
+ for (let i = 0; i < CODE_LEN; i++) s += String(randomInt(0, 10));
8
+ return s;
9
+ }
10
+ function createQqBindRoutes({ query, body, json }) {
11
+ return async function handle({
12
+ path,
13
+ method,
14
+ params,
15
+ req,
16
+ res
17
+ }) {
18
+ if (!path.startsWith("/api/sfmc/qq/bind")) return false;
19
+ if (path === "/api/sfmc/qq/bind/me" && method === "GET") {
20
+ const openid = String(params.get("openid") ?? "").trim();
21
+ const xuid = String(params.get("xuid") ?? "").trim();
22
+ if (!openid && !xuid) {
23
+ json(res, { success: false, error: "openid_or_xuid_required" }, 400);
24
+ return true;
25
+ }
26
+ const rows = openid ? query(SQL`SELECT * FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`) : query(SQL`SELECT * FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`);
27
+ const row = rows[0] ?? null;
28
+ json(res, {
29
+ success: true,
30
+ bound: !!row,
31
+ binding: row ? {
32
+ qq_user_openid: row.qq_user_openid,
33
+ player_xuid: row.player_xuid,
34
+ player_name: row.player_name,
35
+ qq_backend: row.qq_backend,
36
+ bound_at: row.bound_at
37
+ } : null
38
+ });
39
+ return true;
40
+ }
41
+ if (path === "/api/sfmc/qq/bind/request" && method === "POST") {
42
+ const data = await body(req);
43
+ const openid = String(data.openid ?? data.qq_user_openid ?? "").trim();
44
+ const backend = String(data.qq_backend ?? "official").trim() || "official";
45
+ if (!openid) {
46
+ json(res, { success: false, error: "openid_required" }, 400);
47
+ return true;
48
+ }
49
+ const existing = query(
50
+ SQL`SELECT player_name FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`
51
+ );
52
+ if (existing[0]) {
53
+ json(
54
+ res,
55
+ {
56
+ success: false,
57
+ error: "already_bound",
58
+ player_name: existing[0].player_name
59
+ },
60
+ 409
61
+ );
62
+ return true;
63
+ }
64
+ const now = Date.now();
65
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid} OR expires_at < ${now}`);
66
+ let code = "";
67
+ for (let attempt = 0; attempt < 8; attempt++) {
68
+ const candidate = genCode();
69
+ const clash = query(
70
+ SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${candidate} LIMIT 1`
71
+ );
72
+ if (clash.length === 0) {
73
+ code = candidate;
74
+ break;
75
+ }
76
+ }
77
+ if (!code) {
78
+ json(res, { success: false, error: "code_gen_failed" }, 500);
79
+ return true;
80
+ }
81
+ const expiresAt = now + CODE_TTL_MS;
82
+ query(
83
+ SQL`INSERT INTO sfmc_qq_bind_pending (code, qq_user_openid, qq_backend, created_at, expires_at)
84
+ VALUES (${code}, ${openid}, ${backend}, ${now}, ${expiresAt})`
85
+ );
86
+ json(res, { success: true, code, expires_at: expiresAt, ttl_ms: CODE_TTL_MS });
87
+ return true;
88
+ }
89
+ if (path === "/api/sfmc/qq/bind/confirm" && method === "POST") {
90
+ const data = await body(req);
91
+ const code = String(data.code ?? "").trim();
92
+ const xuid = String(data.xuid ?? data.player_xuid ?? "").trim();
93
+ const name = String(data.name ?? data.player_name ?? "").trim();
94
+ if (!code || !xuid) {
95
+ json(res, { success: false, error: "code_and_xuid_required" }, 400);
96
+ return true;
97
+ }
98
+ const now = Date.now();
99
+ const pending = query(
100
+ SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`
101
+ );
102
+ const row = pending[0];
103
+ if (!row) {
104
+ json(res, { success: false, error: "invalid_code" }, 400);
105
+ return true;
106
+ }
107
+ if (Number(row.expires_at) < now) {
108
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);
109
+ json(res, { success: false, error: "code_expired" }, 400);
110
+ return true;
111
+ }
112
+ const byOpenid = query(
113
+ SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`
114
+ );
115
+ if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {
116
+ json(res, { success: false, error: "qq_already_bound", hint: "\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1" }, 409);
117
+ return true;
118
+ }
119
+ const byXuid = query(
120
+ SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`
121
+ );
122
+ if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {
123
+ json(res, { success: false, error: "player_already_bound", hint: "\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ" }, 409);
124
+ return true;
125
+ }
126
+ query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);
127
+ query(
128
+ SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)
129
+ VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`
130
+ );
131
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);
132
+ json(res, {
133
+ success: true,
134
+ binding: {
135
+ qq_user_openid: row.qq_user_openid,
136
+ player_xuid: xuid,
137
+ player_name: name,
138
+ qq_backend: row.qq_backend,
139
+ bound_at: now
140
+ }
141
+ });
142
+ return true;
143
+ }
144
+ if (path === "/api/sfmc/qq/bind/unbind" && method === "POST") {
145
+ const data = await body(req);
146
+ const openid = String(data.openid ?? data.qq_user_openid ?? "").trim();
147
+ const xuid = String(data.xuid ?? data.player_xuid ?? "").trim();
148
+ if (!openid && !xuid) {
149
+ json(res, { success: false, error: "openid_or_xuid_required" }, 400);
150
+ return true;
151
+ }
152
+ let removed = 0;
153
+ if (openid) {
154
+ const before = query(
155
+ SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`
156
+ );
157
+ query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`);
158
+ query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid}`);
159
+ removed += before.length;
160
+ }
161
+ if (xuid) {
162
+ const before = query(
163
+ SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`
164
+ );
165
+ query(SQL`DELETE FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`);
166
+ removed += before.length;
167
+ }
168
+ json(res, { success: true, unbound: removed > 0, removed });
169
+ return true;
170
+ }
171
+ json(res, { success: false, error: "not_found" }, 404);
172
+ return true;
173
+ };
174
+ }
175
+ export {
176
+ CODE_TTL_MS,
177
+ createQqBindRoutes
178
+ };
179
+ //# sourceMappingURL=qq-bind.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/routes/qq-bind.ts"],
4
+ "sourcesContent": ["/**\n * routes/qq-bind.ts \u2014 QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0 HTTP\uFF09\n *\n * POST /api/sfmc/qq/bind/request \u2014 QQ \u4FA7\u7533\u8BF7\u77ED\u7801\n * POST /api/sfmc/qq/bind/confirm \u2014 \u6E38\u620F\u4FA7\u786E\u8BA4\n * POST /api/sfmc/qq/bind/unbind \u2014 \u89E3\u7ED1\n * GET /api/sfmc/qq/bind/me \u2014 \u67E5\u8BE2\n */\n\nimport { randomInt } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nconst CODE_TTL_MS = 5 * 60 * 1000;\nconst CODE_LEN = 6;\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n}\n\nfunction genCode(): string {\n let s = \"\";\n for (let i = 0; i < CODE_LEN; i++) s += String(randomInt(0, 10));\n return s;\n}\n\nfunction createQqBindRoutes({ query, body, json }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (!path.startsWith(\"/api/sfmc/qq/bind\")) return false;\n\n if (path === \"/api/sfmc/qq/bind/me\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n const xuid = String(params.get(\"xuid\") ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n const rows = openid\n ? (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`) as Array<\n Record<string, unknown>\n >)\n : (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`) as Array<\n Record<string, unknown>\n >);\n const row = rows[0] ?? null;\n json(res, {\n success: true,\n bound: !!row,\n binding: row\n ? {\n qq_user_openid: row.qq_user_openid,\n player_xuid: row.player_xuid,\n player_name: row.player_name,\n qq_backend: row.qq_backend,\n bound_at: row.bound_at,\n }\n : null,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/request\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n // \u5DF2\u7ED1\u5B9A\u5219\u62D2\u7EDD\u518D\u7533\u8BF7\uFF08\u987B\u5148\u89E3\u7ED1\uFF09\n const existing = query(\n SQL`SELECT player_name FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`\n ) as Array<{ player_name: string }>;\n if (existing[0]) {\n json(\n res,\n {\n success: false,\n error: \"already_bound\",\n player_name: existing[0].player_name,\n },\n 409\n );\n return true;\n }\n\n const now = Date.now();\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid} OR expires_at < ${now}`);\n\n let code = \"\";\n for (let attempt = 0; attempt < 8; attempt++) {\n const candidate = genCode();\n const clash = query(\n SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${candidate} LIMIT 1`\n ) as unknown[];\n if (clash.length === 0) {\n code = candidate;\n break;\n }\n }\n if (!code) {\n json(res, { success: false, error: \"code_gen_failed\" }, 500);\n return true;\n }\n\n const expiresAt = now + CODE_TTL_MS;\n query(\n SQL`INSERT INTO sfmc_qq_bind_pending (code, qq_user_openid, qq_backend, created_at, expires_at)\n VALUES (${code}, ${openid}, ${backend}, ${now}, ${expiresAt})`\n );\n json(res, { success: true, code, expires_at: expiresAt, ttl_ms: CODE_TTL_MS });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/confirm\" && method === \"POST\") {\n const data = await body(req);\n const code = String(data.code ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n const name = String(data.name ?? data.player_name ?? \"\").trim();\n if (!code || !xuid) {\n json(res, { success: false, error: \"code_and_xuid_required\" }, 400);\n return true;\n }\n\n const now = Date.now();\n const pending = query(\n SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`\n ) as Array<{\n code: string;\n qq_user_openid: string;\n qq_backend: string;\n expires_at: number;\n }>;\n const row = pending[0];\n if (!row) {\n json(res, { success: false, error: \"invalid_code\" }, 400);\n return true;\n }\n if (Number(row.expires_at) < now) {\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, { success: false, error: \"code_expired\" }, 400);\n return true;\n }\n\n const byOpenid = query(\n SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`\n ) as Array<{ player_xuid: string }>;\n if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {\n json(res, { success: false, error: \"qq_already_bound\", hint: \"\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1\" }, 409);\n return true;\n }\n const byXuid = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`\n ) as Array<{ qq_user_openid: string }>;\n if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {\n json(res, { success: false, error: \"player_already_bound\", hint: \"\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ\" }, 409);\n return true;\n }\n\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);\n query(\n SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)\n VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`\n );\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, {\n success: true,\n binding: {\n qq_user_openid: row.qq_user_openid,\n player_xuid: xuid,\n player_name: name,\n qq_backend: row.qq_backend,\n bound_at: now,\n },\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/unbind\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n let removed = 0;\n if (openid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`);\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid}`);\n removed += before.length;\n }\n if (xuid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`);\n removed += before.length;\n }\n json(res, { success: true, unbound: removed > 0, removed });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqBindRoutes, CODE_TTL_MS };\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * routes/qq-events.ts — MC 事件入站
3
+ *
4
+ * POST /api/sfmc/qq/events — 单条或 { events: [...] }(≤100)
5
+ */
6
+ import type { QqEventsAggregator } from "../domain/qq-events.js";
7
+ interface Deps {
8
+ body: (req: import("http").IncomingMessage) => Promise<Record<string, unknown>>;
9
+ json: (res: import("http").ServerResponse, data: Record<string, unknown>, status?: number) => void;
10
+ aggregator: QqEventsAggregator;
11
+ }
12
+ declare function createQqEventsRoutes({ body, json, aggregator }: Deps): ({ path, method, req, res, }: {
13
+ path: string;
14
+ method: string;
15
+ params: URLSearchParams;
16
+ req: import("http").IncomingMessage;
17
+ res: import("http").ServerResponse;
18
+ }) => Promise<boolean>;
19
+ export { createQqEventsRoutes };
20
+ //# sourceMappingURL=qq-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-events.d.ts","sourceRoot":"","sources":["../../src/routes/qq-events.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,wBAAwB,CAAC;AAGjF,UAAU,IAAI;IACZ,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnG,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAaD,iBAAS,oBAAoB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,iCAMzD;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAwBrB;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { normalizeEventPayload } from "../domain/qq-events.js";
2
+ function collectPayloads(data) {
3
+ if (Array.isArray(data.events)) {
4
+ return data.events.map((x) => normalizeEventPayload(x)).filter((x) => x != null);
5
+ }
6
+ const one = normalizeEventPayload(data);
7
+ return one ? [one] : [];
8
+ }
9
+ function createQqEventsRoutes({ body, json, aggregator }) {
10
+ return async function handle({
11
+ path,
12
+ method,
13
+ req,
14
+ res
15
+ }) {
16
+ if (path !== "/api/sfmc/qq/events") return false;
17
+ if (method !== "POST") {
18
+ json(res, { success: false, error: "not_found" }, 404);
19
+ return true;
20
+ }
21
+ const data = await body(req);
22
+ if (Array.isArray(data.events) && data.events.length > 100) {
23
+ json(res, { success: false, error: "too many requests" }, 413);
24
+ return true;
25
+ }
26
+ const payloads = collectPayloads(data);
27
+ if (payloads.length === 0) {
28
+ json(res, { success: false, error: "invalid" }, 400);
29
+ return true;
30
+ }
31
+ const result = aggregator.ingestMany(payloads);
32
+ json(res, { success: true, ...result });
33
+ return true;
34
+ };
35
+ }
36
+ export {
37
+ createQqEventsRoutes
38
+ };
39
+ //# sourceMappingURL=qq-events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/routes/qq-events.ts"],
4
+ "sourcesContent": ["/**\n * routes/qq-events.ts \u2014 MC \u4E8B\u4EF6\u5165\u7AD9\n *\n * POST /api/sfmc/qq/events \u2014 \u5355\u6761\u6216 { events: [...] }\uFF08\u2264100\uFF09\n */\n\nimport type { QqEventsAggregator, QqEventPayload } from \"../domain/qq-events.js\";\nimport { normalizeEventPayload } from \"../domain/qq-events.js\";\n\ninterface Deps {\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n aggregator: QqEventsAggregator;\n}\n\nfunction collectPayloads(data: Record<string, unknown>): QqEventPayload[] {\n if (Array.isArray(data.events)) {\n return (data.events as unknown[])\n .map((x) => normalizeEventPayload(x))\n .filter((x): x is QqEventPayload => x != null);\n }\n // \u5355\u6761\uFF1A\u9876\u5C42\u5373\u4E8B\u4EF6\n const one = normalizeEventPayload(data);\n return one ? [one] : [];\n}\n\nfunction createQqEventsRoutes({ body, json, aggregator }: Deps) {\n return async function handle({\n path,\n method,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (path !== \"/api/sfmc/qq/events\") return false;\n\n if (method !== \"POST\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n\n const data = await body(req);\n if (Array.isArray(data.events) && (data.events as unknown[]).length > 100) {\n json(res, { success: false, error: \"too many requests\" }, 413);\n return true;\n }\n\n const payloads = collectPayloads(data);\n if (payloads.length === 0) {\n json(res, { success: false, error: \"invalid\" }, 400);\n return true;\n }\n\n const result = aggregator.ingestMany(payloads);\n json(res, { success: true, ...result });\n return true;\n };\n}\n\nexport { createQqEventsRoutes };\n"],
5
+ "mappings": "AAOA,SAAS,6BAA6B;AAQtC,SAAS,gBAAgB,MAAiD;AACxE,MAAI,MAAM,QAAQ,KAAK,MAAM,GAAG;AAC9B,WAAQ,KAAK,OACV,IAAI,CAAC,MAAM,sBAAsB,CAAC,CAAC,EACnC,OAAO,CAAC,MAA2B,KAAK,IAAI;AAAA,EACjD;AAEA,QAAM,MAAM,sBAAsB,IAAI;AACtC,SAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACxB;AAEA,SAAS,qBAAqB,EAAE,MAAM,MAAM,WAAW,GAAS;AAC9D,SAAO,eAAe,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAMqB;AACnB,QAAI,SAAS,sBAAuB,QAAO;AAE3C,QAAI,WAAW,QAAQ;AACrB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,KAAK,GAAG;AAC3B,QAAI,MAAM,QAAQ,KAAK,MAAM,KAAM,KAAK,OAAqB,SAAS,KAAK;AACzE,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,oBAAoB,GAAG,GAAG;AAC7D,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,gBAAgB,IAAI;AACrC,QAAI,SAAS,WAAW,GAAG;AACzB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,UAAU,GAAG,GAAG;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,WAAW,WAAW,QAAQ;AAC7C,SAAK,KAAK,EAAE,SAAS,MAAM,GAAG,OAAO,CAAC;AACtC,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * routes/qq-join.ts — 入服审批状态机 + BDS 生效队列(不写 allowlist 文件)
3
+ *
4
+ * GET /api/sfmc/qq/join/settings
5
+ * POST /api/sfmc/qq/join/settings
6
+ * POST /api/sfmc/qq/join/request
7
+ * POST /api/sfmc/qq/join/decide
8
+ * GET /api/sfmc/qq/join/pending
9
+ * GET /api/sfmc/qq/join/apply-queue
10
+ * POST /api/sfmc/qq/join/applied
11
+ * POST /api/sfmc/qq/admin/kick
12
+ * GET /api/sfmc/qq/admin/action-queue
13
+ * POST /api/sfmc/qq/admin/action-done
14
+ */
15
+ import type { QueryFn } from "../lib/sqlite.js";
16
+ export type JoinFeatureFlags = {
17
+ /** 入服白名单总开关 */
18
+ allowlistEnabled: boolean;
19
+ /** 是否需要管理员审批 */
20
+ requireApproval: boolean;
21
+ /**
22
+ * 是否将 QQ 群主/群管理员视作 SFMC 管理员。
23
+ * 仅 configs/qq_link.json 可改,API/群聊不可写。
24
+ */
25
+ treatGroupAdminsAsAdmins: boolean;
26
+ };
27
+ interface Deps {
28
+ query: QueryFn;
29
+ body: (req: import("http").IncomingMessage) => Promise<Record<string, unknown>>;
30
+ json: (res: import("http").ServerResponse, data: Record<string, unknown>, status?: number) => void;
31
+ /** 当前管理员 openid 列表(来自 qq_config) */
32
+ getAdminOpenids: () => string[];
33
+ /** 读取入服开关(应读最新 qq_config) */
34
+ getJoinFlags: () => JoinFeatureFlags;
35
+ /** 写回入服开关(持久化 + 内存) */
36
+ setJoinFlags: (partial: Partial<JoinFeatureFlags>) => JoinFeatureFlags;
37
+ }
38
+ declare function createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }: Deps): ({ path, method, params, req, res, }: {
39
+ path: string;
40
+ method: string;
41
+ params: URLSearchParams;
42
+ req: import("http").IncomingMessage;
43
+ res: import("http").ServerResponse;
44
+ }) => Promise<boolean>;
45
+ export { createQqJoinRoutes };
46
+ //# sourceMappingURL=qq-join.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-join.d.ts","sourceRoot":"","sources":["../../src/routes/qq-join.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,eAAe;IACf,gBAAgB,EAAE,OAAO,CAAC;IAC1B,gBAAgB;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,wBAAwB,EAAE,OAAO,CAAC;CACnC,CAAC;AAEF,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnG,oCAAoC;IACpC,eAAe,EAAE,MAAM,MAAM,EAAE,CAAC;IAChC,6BAA6B;IAC7B,YAAY,EAAE,MAAM,gBAAgB,CAAC;IACrC,uBAAuB;IACvB,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,gBAAgB,CAAC,KAAK,gBAAgB,CAAC;CACxE;AA4BD,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,IAAI,yCAO/F;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAkRrB;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,277 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { SQL } from "sql-template-strings";
3
+ function newId(prefix) {
4
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
5
+ }
6
+ function isAdmin(getAdminOpenids, openid) {
7
+ const list = getAdminOpenids();
8
+ if (!openid || list.length === 0) return false;
9
+ return list.includes(openid);
10
+ }
11
+ function authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data) {
12
+ if (isAdmin(getAdminOpenids, openid)) return true;
13
+ const asGroup = data.as_group_admin === true || data.as_group_admin === "true" || data.is_group_admin === true || data.is_group_admin === "true";
14
+ return asGroup && getJoinFlags().treatGroupAdminsAsAdmins === true;
15
+ }
16
+ function createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }) {
17
+ return async function handle({
18
+ path,
19
+ method,
20
+ params,
21
+ req,
22
+ res
23
+ }) {
24
+ const isJoin = path.startsWith("/api/sfmc/qq/join");
25
+ const isAdminPath = path.startsWith("/api/sfmc/qq/admin");
26
+ if (!isJoin && !isAdminPath) return false;
27
+ if (path === "/api/sfmc/qq/join/settings" && method === "GET") {
28
+ const flags = getJoinFlags();
29
+ json(res, {
30
+ success: true,
31
+ allowlist_enabled: flags.allowlistEnabled,
32
+ require_approval: flags.requireApproval,
33
+ treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins
34
+ });
35
+ return true;
36
+ }
37
+ if (path === "/api/sfmc/qq/join/settings" && method === "POST") {
38
+ const data = await body(req);
39
+ const openid = String(data.openid ?? data.decided_by ?? "").trim();
40
+ if (Object.prototype.hasOwnProperty.call(data, "treat_group_admins_as_admins") || Object.prototype.hasOwnProperty.call(data, "treatGroupAdminsAsAdmins")) {
41
+ json(
42
+ res,
43
+ {
44
+ success: false,
45
+ error: "immutable_field",
46
+ note: "treat_group_admins_as_admins \u53EA\u80FD\u6539 configs/qq_link.json\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539"
47
+ },
48
+ 400
49
+ );
50
+ return true;
51
+ }
52
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {
53
+ json(res, { success: false, error: "not_admin" }, 403);
54
+ return true;
55
+ }
56
+ const partial = {};
57
+ if (Object.prototype.hasOwnProperty.call(data, "allowlist_enabled")) {
58
+ partial.allowlistEnabled = data.allowlist_enabled === true || data.allowlist_enabled === "true";
59
+ }
60
+ if (Object.prototype.hasOwnProperty.call(data, "require_approval")) {
61
+ partial.requireApproval = data.require_approval === true || data.require_approval === "true";
62
+ }
63
+ if (partial.allowlistEnabled === void 0 && partial.requireApproval === void 0) {
64
+ json(res, { success: false, error: "no_settings" }, 400);
65
+ return true;
66
+ }
67
+ const flags = setJoinFlags(partial);
68
+ json(res, {
69
+ success: true,
70
+ allowlist_enabled: flags.allowlistEnabled,
71
+ require_approval: flags.requireApproval,
72
+ treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins
73
+ });
74
+ return true;
75
+ }
76
+ if (path === "/api/sfmc/qq/join/request" && method === "POST") {
77
+ const flags = getJoinFlags();
78
+ if (!flags.allowlistEnabled) {
79
+ json(res, { success: false, error: "join_allowlist_disabled" }, 403);
80
+ return true;
81
+ }
82
+ const data = await body(req);
83
+ const openid = String(data.openid ?? data.applicant_openid ?? "").trim();
84
+ const playerName = String(data.player_name ?? data.name ?? "").trim();
85
+ const backend = String(data.qq_backend ?? "official").trim() || "official";
86
+ if (!openid) {
87
+ json(res, { success: false, error: "openid_required" }, 400);
88
+ return true;
89
+ }
90
+ if (!playerName || playerName.length > 32) {
91
+ json(res, { success: false, error: "player_name_required" }, 400);
92
+ return true;
93
+ }
94
+ const id = newId("join");
95
+ const now = Date.now();
96
+ const autoApproved = !flags.requireApproval;
97
+ const status = autoApproved ? "approved" : "pending";
98
+ if (autoApproved) {
99
+ query(SQL`
100
+ INSERT INTO sfmc_qq_join_requests
101
+ (id, applicant_openid, player_name, status, qq_backend, created_at, decided_at, decided_by)
102
+ VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now}, ${now}, 'auto')
103
+ `);
104
+ } else {
105
+ query(SQL`
106
+ INSERT INTO sfmc_qq_join_requests
107
+ (id, applicant_openid, player_name, status, qq_backend, created_at)
108
+ VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now})
109
+ `);
110
+ }
111
+ const admins = getAdminOpenids();
112
+ json(res, {
113
+ success: true,
114
+ id,
115
+ player_name: playerName,
116
+ status,
117
+ auto_approved: autoApproved,
118
+ admin_notify: !autoApproved && admins.length > 0,
119
+ note: autoApproved ? "\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355" : admins.length === 0 ? "\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458" : void 0
120
+ });
121
+ return true;
122
+ }
123
+ if (path === "/api/sfmc/qq/join/decide" && method === "POST") {
124
+ const data = await body(req);
125
+ const id = String(data.id ?? data.request_id ?? "").trim();
126
+ const decision = String(data.decision ?? "").trim().toLowerCase();
127
+ const decidedBy = String(data.decided_by ?? data.openid ?? "").trim();
128
+ if (!id || decision !== "approve" && decision !== "reject") {
129
+ json(res, { success: false, error: "id_and_decision_required" }, 400);
130
+ return true;
131
+ }
132
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, decidedBy, data)) {
133
+ json(res, { success: false, error: "not_admin" }, 403);
134
+ return true;
135
+ }
136
+ const rows = query(SQL`SELECT * FROM sfmc_qq_join_requests WHERE id = ${id} LIMIT 1`);
137
+ const row = rows[0];
138
+ if (!row) {
139
+ json(res, { success: false, error: "not_found" }, 404);
140
+ return true;
141
+ }
142
+ if (String(row.status) !== "pending") {
143
+ json(res, { success: false, error: "already_decided", status: row.status }, 409);
144
+ return true;
145
+ }
146
+ const next = decision === "approve" ? "approved" : "rejected";
147
+ const now = Date.now();
148
+ query(SQL`
149
+ UPDATE sfmc_qq_join_requests
150
+ SET status = ${next}, decided_at = ${now}, decided_by = ${decidedBy}
151
+ WHERE id = ${id}
152
+ `);
153
+ json(res, {
154
+ success: true,
155
+ id,
156
+ status: next,
157
+ player_name: row.player_name,
158
+ applicant_openid: row.applicant_openid
159
+ });
160
+ return true;
161
+ }
162
+ if (path === "/api/sfmc/qq/join/pending" && method === "GET") {
163
+ const openid = String(params.get("openid") ?? "").trim();
164
+ if (openid) {
165
+ const asGroup = params.get("as_group_admin") === "true" || params.get("is_group_admin") === "true";
166
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, { as_group_admin: asGroup })) {
167
+ json(res, { success: false, error: "not_admin" }, 403);
168
+ return true;
169
+ }
170
+ }
171
+ const rows = query(
172
+ SQL`SELECT id, applicant_openid, player_name, status, created_at
173
+ FROM sfmc_qq_join_requests WHERE status = 'pending'
174
+ ORDER BY created_at ASC LIMIT 50`
175
+ );
176
+ json(res, { success: true, pending: rows });
177
+ return true;
178
+ }
179
+ if (path === "/api/sfmc/qq/join/apply-queue" && method === "GET") {
180
+ const flags = getJoinFlags();
181
+ if (!flags.allowlistEnabled) {
182
+ json(res, { success: true, queue: [], note: "join_allowlist_disabled" });
183
+ return true;
184
+ }
185
+ const rows = query(
186
+ SQL`SELECT id, player_name, applicant_openid, decided_at
187
+ FROM sfmc_qq_join_requests WHERE status = 'approved'
188
+ ORDER BY decided_at ASC LIMIT 32`
189
+ );
190
+ json(res, { success: true, queue: rows });
191
+ return true;
192
+ }
193
+ if (path === "/api/sfmc/qq/join/applied" && method === "POST") {
194
+ const data = await body(req);
195
+ const id = String(data.id ?? "").trim();
196
+ const ok = data.ok !== false && data.error == null;
197
+ const err = String(data.error ?? "").slice(0, 200);
198
+ if (!id) {
199
+ json(res, { success: false, error: "id_required" }, 400);
200
+ return true;
201
+ }
202
+ const now = Date.now();
203
+ if (ok) {
204
+ query(SQL`
205
+ UPDATE sfmc_qq_join_requests
206
+ SET status = 'applied', applied_at = ${now}, apply_error = NULL
207
+ WHERE id = ${id} AND status = 'approved'
208
+ `);
209
+ } else {
210
+ query(SQL`
211
+ UPDATE sfmc_qq_join_requests
212
+ SET status = 'apply_failed', applied_at = ${now}, apply_error = ${err}
213
+ WHERE id = ${id} AND status = 'approved'
214
+ `);
215
+ }
216
+ json(res, { success: true });
217
+ return true;
218
+ }
219
+ if (path === "/api/sfmc/qq/admin/kick" && method === "POST") {
220
+ const data = await body(req);
221
+ const openid = String(data.openid ?? data.requested_by ?? "").trim();
222
+ const target = String(data.target_name ?? data.name ?? "").trim();
223
+ const reason = String(data.reason ?? "QQ \u7BA1\u7406\u5458\u8E22\u51FA").trim().slice(0, 80);
224
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {
225
+ json(res, { success: false, error: "not_admin" }, 403);
226
+ return true;
227
+ }
228
+ if (!target) {
229
+ json(res, { success: false, error: "target_name_required" }, 400);
230
+ return true;
231
+ }
232
+ const id = newId("act");
233
+ const now = Date.now();
234
+ query(SQL`
235
+ INSERT INTO sfmc_qq_admin_actions
236
+ (id, kind, target_name, reason, requested_by, status, created_at)
237
+ VALUES (${id}, 'kick', ${target}, ${reason}, ${openid}, 'pending', ${now})
238
+ `);
239
+ json(res, { success: true, id });
240
+ return true;
241
+ }
242
+ if (path === "/api/sfmc/qq/admin/action-queue" && method === "GET") {
243
+ const rows = query(
244
+ SQL`SELECT id, kind, target_name, reason, created_at
245
+ FROM sfmc_qq_admin_actions WHERE status = 'pending'
246
+ ORDER BY created_at ASC LIMIT 32`
247
+ );
248
+ json(res, { success: true, queue: rows });
249
+ return true;
250
+ }
251
+ if (path === "/api/sfmc/qq/admin/action-done" && method === "POST") {
252
+ const data = await body(req);
253
+ const id = String(data.id ?? "").trim();
254
+ const ok = data.ok !== false && data.error == null;
255
+ const err = String(data.error ?? "").slice(0, 200);
256
+ if (!id) {
257
+ json(res, { success: false, error: "id_required" }, 400);
258
+ return true;
259
+ }
260
+ const now = Date.now();
261
+ const status = ok ? "done" : "failed";
262
+ query(SQL`
263
+ UPDATE sfmc_qq_admin_actions
264
+ SET status = ${status}, applied_at = ${now}, apply_error = ${ok ? "" : err}
265
+ WHERE id = ${id} AND status = 'pending'
266
+ `);
267
+ json(res, { success: true });
268
+ return true;
269
+ }
270
+ json(res, { success: false, error: "not_found" }, 404);
271
+ return true;
272
+ };
273
+ }
274
+ export {
275
+ createQqJoinRoutes
276
+ };
277
+ //# sourceMappingURL=qq-join.js.map