@sfmc-bds/db-server 0.2.0-beta.4 → 0.2.0-beta.6
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.
- package/LICENSE +663 -0
- package/dist/domain/bridge.d.ts +57 -23
- package/dist/domain/bridge.d.ts.map +1 -1
- package/dist/domain/bridge.js +77 -6
- package/dist/domain/bridge.js.map +2 -2
- package/dist/domain/bridge.test.d.ts +2 -0
- package/dist/domain/bridge.test.d.ts.map +1 -0
- package/dist/domain/bridge.test.js +87 -0
- package/dist/domain/bridge.test.js.map +7 -0
- package/dist/domain/economy.d.ts +38 -7
- package/dist/domain/economy.d.ts.map +1 -1
- package/dist/domain/economy.js.map +2 -2
- package/dist/domain/messages-bridge-filter.test.d.ts +5 -0
- package/dist/domain/messages-bridge-filter.test.d.ts.map +1 -0
- package/dist/domain/messages-bridge-filter.test.js +20 -0
- package/dist/domain/messages-bridge-filter.test.js.map +7 -0
- package/dist/domain/qq-bind-status.test.d.ts +2 -0
- package/dist/domain/qq-bind-status.test.d.ts.map +1 -0
- package/dist/domain/qq-bind-status.test.js +211 -0
- package/dist/domain/qq-bind-status.test.js.map +7 -0
- package/dist/domain/qq-events.d.ts +84 -0
- package/dist/domain/qq-events.d.ts.map +1 -0
- package/dist/domain/qq-events.js +193 -0
- package/dist/domain/qq-events.js.map +7 -0
- package/dist/domain/qq-events.test.d.ts +5 -0
- package/dist/domain/qq-events.test.d.ts.map +1 -0
- package/dist/domain/qq-events.test.js +170 -0
- package/dist/domain/qq-events.test.js.map +7 -0
- package/dist/domain/qq-join.test.d.ts +2 -0
- package/dist/domain/qq-join.test.d.ts.map +1 -0
- package/dist/domain/qq-join.test.js +353 -0
- package/dist/domain/qq-join.test.js.map +7 -0
- package/dist/domain/schema.d.ts +17 -19
- package/dist/domain/schema.d.ts.map +1 -1
- package/dist/domain/schema.js +69 -0
- package/dist/domain/schema.js.map +2 -2
- package/dist/domain/system-status.d.ts +75 -0
- package/dist/domain/system-status.d.ts.map +1 -0
- package/dist/domain/system-status.js +181 -0
- package/dist/domain/system-status.js.map +7 -0
- package/dist/domain/system-status.test.d.ts +5 -0
- package/dist/domain/system-status.test.d.ts.map +1 -0
- package/dist/domain/system-status.test.js +19 -0
- package/dist/domain/system-status.test.js.map +7 -0
- package/dist/domain/transaction.d.ts +5 -4
- package/dist/domain/transaction.d.ts.map +1 -1
- package/dist/domain/transaction.js.map +2 -2
- package/dist/env.d.ts +7 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +55 -3
- package/dist/env.js.map +2 -2
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +105 -19
- package/dist/index.js.map +2 -2
- package/dist/lib/idempotency-store.d.ts +17 -11
- package/dist/lib/idempotency-store.d.ts.map +1 -1
- package/dist/lib/idempotency-store.js.map +2 -2
- package/dist/lib/idempotency.d.ts +3 -4
- package/dist/lib/idempotency.d.ts.map +1 -1
- package/dist/lib/idempotency.js.map +2 -2
- package/dist/lib/module-state.d.ts +38 -8
- package/dist/lib/module-state.d.ts.map +1 -1
- package/dist/lib/module-state.js.map +2 -2
- package/dist/lib/order-by.d.ts +8 -6
- package/dist/lib/order-by.d.ts.map +1 -1
- package/dist/lib/order-by.js.map +2 -2
- package/dist/lib/sql-helpers.d.ts +25 -13
- package/dist/lib/sql-helpers.d.ts.map +1 -1
- package/dist/lib/sql-helpers.js.map +2 -2
- package/dist/lib/sqlite.d.ts +14 -5
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js.map +2 -2
- package/dist/manifest-loader.d.ts +5 -2
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +16 -6
- package/dist/manifest-loader.js.map +2 -2
- package/dist/manifest.d.ts +1 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +13 -8
- package/dist/manifest.js.map +2 -2
- package/dist/project-root.d.ts +1 -1
- package/dist/project-root.d.ts.map +1 -1
- package/dist/project-root.js +2 -2
- package/dist/project-root.js.map +2 -2
- package/dist/routes/_shared.d.ts +14 -5
- package/dist/routes/_shared.d.ts.map +1 -1
- package/dist/routes/_shared.js.map +2 -2
- package/dist/routes/config.d.ts +8 -11
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +17 -23
- package/dist/routes/config.js.map +2 -2
- package/dist/routes/db-routes.d.ts +6 -0
- package/dist/routes/db-routes.d.ts.map +1 -1
- package/dist/routes/db-routes.js.map +2 -2
- package/dist/routes/messages.d.ts +6 -2
- package/dist/routes/messages.d.ts.map +1 -1
- package/dist/routes/messages.js +17 -3
- package/dist/routes/messages.js.map +2 -2
- package/dist/routes/module-config-routes.d.ts +13 -9
- package/dist/routes/module-config-routes.d.ts.map +1 -1
- package/dist/routes/module-config-routes.js.map +2 -2
- package/dist/routes/modules.d.ts +6 -0
- package/dist/routes/modules.d.ts.map +1 -1
- package/dist/routes/modules.js.map +2 -2
- package/dist/routes/qq-bind.d.ts +24 -0
- package/dist/routes/qq-bind.d.ts.map +1 -0
- package/dist/routes/qq-bind.js +179 -0
- package/dist/routes/qq-bind.js.map +7 -0
- package/dist/routes/qq-events.d.ts +20 -0
- package/dist/routes/qq-events.d.ts.map +1 -0
- package/dist/routes/qq-events.js +39 -0
- package/dist/routes/qq-events.js.map +7 -0
- package/dist/routes/qq-join.d.ts +46 -0
- package/dist/routes/qq-join.d.ts.map +1 -0
- package/dist/routes/qq-join.js +277 -0
- package/dist/routes/qq-join.js.map +7 -0
- package/dist/routes/service-routes.d.ts +6 -0
- package/dist/routes/service-routes.d.ts.map +1 -1
- package/dist/routes/service-routes.js.map +2 -2
- package/dist/routes/status.d.ts +19 -0
- package/dist/routes/status.d.ts.map +1 -0
- package/dist/routes/status.js +74 -0
- package/dist/routes/status.js.map +7 -0
- package/dist/schema-registry.d.ts +16 -15
- package/dist/schema-registry.d.ts.map +1 -1
- package/dist/schema-registry.js +7 -1
- package/dist/schema-registry.js.map +2 -2
- package/dist/server.d.ts +19 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js.map +2 -2
- package/dist/service-registry.d.ts +39 -9
- package/dist/service-registry.d.ts.map +1 -1
- package/dist/service-registry.js +30 -2
- package/dist/service-registry.js.map +2 -2
- package/dist/services/builtin-handlers.d.ts +27 -7
- package/dist/services/builtin-handlers.d.ts.map +1 -1
- package/dist/services/builtin-handlers.js.map +2 -2
- package/dist/tx-runner.d.ts +29 -3
- package/dist/tx-runner.d.ts.map +1 -1
- package/dist/tx-runner.js +29 -3
- package/dist/tx-runner.js.map +2 -2
- package/package.json +17 -15
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/domain/economy.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * domain/economy.ts \u2014 \u7ECF\u6D4E\u7CFB\u7EDF\u4E1A\u52A1\u6838\u5FC3\n *\n * \u63D0\u4F9B\u8D26\u6237 upsert\u3001\u901A\u7528\u8F6C\u8D26\u3001\u65E5\u5E38\u4EFB\u52A1\u5956\u52B1\u4E0E\u6708\u5EA6\u7EDF\u8BA1\u3002\n * land / chat / gui \u7B49\u901A\u8FC7 service(economy.account.*) \u8C03\u7528\u672C\u6587\u4EF6\u3002\n *\n * \u4E8B\u52A1\u7EA6\u5B9A(\u65B9\u6848 A):\n * - applyEconomySteps / submitDailyTaskSteps \u2014 \u65E0 BEGIN/COMMIT,\u53EF\u5D4C\u8FDB\u5916\u5C42 db.tx\n * - applyEconomyTransaction / submitDailyTaskTx \u2014 \u72EC\u7ACB\u8C03\u7528\u65F6\u81EA\u5DF1\u5F00\u4E8B\u52A1;\n * alreadyInTx=true \u65F6\u53EA\u8DD1 steps\n *\n * \u884C\u7C7B\u578B\u5728\u6B64\u672C\u5730\u5B9A\u4E49\uFF08\u5E73\u53F0\u4E0D\u4F9D\u8D56\u4E1A\u52A1\u6A21\u5757\u5305\uFF1B\u6743\u5A01\u526F\u672C\u5728 sfmc-modules/packages/economy\uFF09\u3002\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport type { SQLStatement } from \"sql-template-strings\";\nimport { isValidIdempotencyKey } from \"../lib/idempotency.js\";\nimport { sql } from \"../lib/sql-helpers.js\";\nimport type { TxResult } from \"./transaction.js\";\nexport type { TxResult };\n\n/**\n * \u7ECF\u6D4E\u57DF\u884C\u6A21\u578B\uFF08\u539F SDK contracts/economy\uFF1B\u5951\u7EA6\u5185\u805A\u5230\u8868\u6240\u6709\u8005 db-server\uFF09\u3002\n * \u4E1A\u52A1\u6A21\u5757\u5E94\u901A\u8FC7 service(economy.*) \u6D88\u8D39\u89C6\u56FE\uFF0C\u4E0D\u4F9D\u8D56\u8FD9\u4E9B\u5185\u90E8\u884C\u7C7B\u578B\u3002\n */\nexport interface EconomyAccountRow {\n player_id: string;\n player_name_snapshot: string;\n balance: number;\n version: number;\n created_at: number;\n updated_at: number;\n}\n\n/** \u5E42\u7B49\u952E\u8868\u884C */\nexport interface EconomyIdempotencyRow {\n actor_id: string;\n idempotency_key: string;\n transaction_id: string;\n response_json: string;\n created_at: number;\n}\n\n/** \u6D41\u6C34\u8868\u884C */\nexport interface EconomyTransactionRow {\n id: string;\n transaction_type: string;\n actor_id: string;\n source_player_id?: string;\n target_player_id?: string;\n amount: number;\n balance_before?: number;\n balance_after?: number;\n reference_type: string;\n reference_id: string;\n reason: string;\n created_at: number;\n idempotency_key: string;\n\n /** JS-side alias fields used by domain/economy.ts */\n actorId?: string;\n type?: string;\n referenceType?: string;\n referenceId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n}\n\nconst TABLE_ACCOUNTS = \"sfmc_economy_accounts\";\nconst TABLE_TRANSACTIONS = \"sfmc_economy_transactions\";\nconst TABLE_IDEMPOTENCY = \"sfmc_economy_idempotency\";\nconst TABLE_DAILY = \"sfmc_economy_daily_tasks\";\nconst TABLE_STATS = \"sfmc_economy_stats\";\n\n/** query \u517C\u5BB9\u683C\u5F0F\uFF1Astring / SQLStatement / { sql, values } */\nexport type AnyQuery = (\n sql: string | SQLStatement | { sql: string; values?: unknown[] },\n values?: unknown[]\n) => unknown[] | { changes: number | bigint };\n\nfunction newTransactionId(now: number): string {\n return `E${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * \u7ECF\u6D4E\u8D26\u6237 DB \u884C\uFF08snake_case\uFF0C\u5BF9\u5E94 sfmc_economy_accounts\uFF09\u3002\n * \u4E1A\u52A1\u57DF\u7C7B\u578B\u7531\u672C\u6A21\u5757\u7EF4\u62A4\uFF0C\u4E0D\u518D\u4F9D\u8D56 sdk/contracts\u3002\n */\nexport interface EconomyAccountRow {\n player_id: string;\n player_name_snapshot: string;\n balance: number;\n version: number;\n created_at: number;\n updated_at: number;\n}\n\n/**\n * \u7ECF\u6D4E\u4EA4\u6613\u6D41\u6C34 DB \u884C\uFF08snake_case\uFF0C\u5BF9\u5E94 sfmc_economy_transactions\uFF09\u3002\n * \u542B\u5386\u53F2 JS \u4FA7\u522B\u540D\u5B57\u6BB5\uFF0C\u4F9B\u8C03\u7528\u65B9\u517C\u5BB9\u8BFB\u53D6\u3002\n */\nexport interface EconomyTransactionRow {\n id: string;\n transaction_type: string;\n actor_id: string;\n source_player_id?: string;\n target_player_id?: string;\n amount: number;\n balance_before?: number;\n balance_after?: number;\n reference_type: string;\n reference_id: string;\n reason: string;\n created_at: number;\n idempotency_key: string;\n\n /** JS-side alias fields used by domain/economy.ts */\n actorId?: string;\n type?: string;\n referenceType?: string;\n referenceId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n}\n\nexport interface EconomyAccountView {\n playerId: string;\n playerName: string;\n balance: number;\n version: number;\n}\n\nexport function economyResult(row: EconomyAccountRow | undefined): EconomyAccountView | undefined {\n if (!row) return undefined;\n return {\n playerId: row.player_id,\n playerName: row.player_name_snapshot,\n balance: row.balance,\n version: row.version,\n };\n}\n\n/** \u786E\u4FDD\u8D26\u6237\u5B58\u5728\u5E76\u8FD4\u56DE\u6700\u65B0\u884C */\nexport function ensureEconomyAccount(\n query: AnyQuery,\n playerId: string,\n playerName: string\n): EconomyAccountRow | undefined {\n const now = Date.now();\n query(\n sql(\n `INSERT INTO ${TABLE_ACCOUNTS} (player_id, player_name_snapshot, balance, version, created_at, updated_at)\n VALUES (?, ?, 0, 1, ?, ?)\n ON CONFLICT(player_id) DO UPDATE SET\n player_name_snapshot = excluded.player_name_snapshot,\n updated_at = excluded.updated_at`,\n [String(playerId), String(playerName), now, now]\n )\n );\n const rows = query(sql(`SELECT * FROM ${TABLE_ACCOUNTS} WHERE player_id = ?`, [String(playerId)]));\n if (Array.isArray(rows)) {\n return (rows as EconomyAccountRow[])[0];\n }\n return undefined;\n}\n\n/** \u8BFB\u53D6(\u5FC5\u8981\u65F6\u521B\u5EFA)\u8D26\u6237\u4E1A\u52A1\u89C6\u56FE */\nexport function getEconomyAccount(query: AnyQuery, playerId: string, playerName = \"\"): EconomyAccountView | null {\n const row = ensureEconomyAccount(query, playerId, playerName);\n return economyResult(row) ?? null;\n}\n\nexport interface ApplyEconomyInput {\n actor_id?: string;\n amount: number;\n transaction_type?: string;\n source_player_id?: string;\n target_player_id?: string;\n reference_type?: string;\n reference_id?: string;\n reason?: string;\n idempotency_key?: string;\n\n actorId?: string;\n type?: string;\n sourcePlayerId?: string;\n targetPlayerId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n referenceType?: string;\n referenceId?: string;\n reasonAlias?: string;\n idempotencyKey?: string;\n}\n\nexport type ApplyEconomyResult =\n | {\n ok: true;\n transactionId: string;\n replayed?: boolean;\n source:\n | (EconomyAccountView & {\n balanceBefore: number;\n balanceAfter: number;\n })\n | null;\n target:\n | (EconomyAccountView & {\n balanceBefore: number;\n balanceAfter: number;\n })\n | null;\n }\n | {\n ok: false;\n error: string;\n status: number;\n balance?: number;\n };\n\n/**\n * \u7ECF\u6D4E\u53D8\u66F4\u6838\u5FC3(\u65E0 BEGIN/COMMIT)\u3002\n * \u7EAF debit(\u4EC5 source)\u6216\u7EAF credit(\u4EC5 target)\u5747\u53EF\u6210\u529F\u3002\n */\nexport function applyEconomySteps(query: AnyQuery, data: ApplyEconomyInput): ApplyEconomyResult {\n const amount = Number(data.amount);\n const actorId = String(data.actor_id ?? data.actorId ?? \"\").trim();\n if (!actorId || !Number.isSafeInteger(amount) || amount <= 0) {\n return { ok: false, error: \"invalid_amount\", status: 400 };\n }\n\n const sourceId = data.source_player_id\n ? String(data.source_player_id)\n : data.sourcePlayerId\n ? String(data.sourcePlayerId)\n : null;\n const targetId = data.target_player_id\n ? String(data.target_player_id)\n : data.targetPlayerId\n ? String(data.targetPlayerId)\n : null;\n\n const idempotencyKey = String(data.idempotency_key ?? data.idempotencyKey ?? \"\").trim();\n if (idempotencyKey && !isValidIdempotencyKey(idempotencyKey)) {\n return { ok: false, error: \"invalid_idempotency_key\", status: 400 };\n }\n if (!sourceId && !targetId) {\n return { ok: false, error: \"missing_account\", status: 400 };\n }\n if (sourceId && sourceId !== actorId) {\n return { ok: false, error: \"forbidden_source\", status: 403 };\n }\n if (sourceId && targetId && sourceId === targetId) {\n return { ok: false, error: \"same_account\", status: 400 };\n }\n\n // \u5E42\u7B49\u56DE\u653E\n if (idempotencyKey) {\n const rows = query(\n sql(\n `SELECT response_json FROM ${TABLE_IDEMPOTENCY}\n WHERE actor_id = ? AND idempotency_key = ?`,\n [actorId, idempotencyKey]\n )\n );\n const previous = Array.isArray(rows) ? (rows as Array<{ response_json: string }>)[0] : undefined;\n if (previous) {\n return { ...JSON.parse(previous.response_json), replayed: true } as ApplyEconomyResult;\n }\n }\n\n const source = sourceId ? ensureEconomyAccount(query, sourceId, String(data.sourcePlayerName ?? \"\")) : null;\n const target = targetId ? ensureEconomyAccount(query, targetId, String(data.targetPlayerName ?? \"\")) : null;\n if (sourceId && !source) {\n return { ok: false, error: \"source_not_found\", status: 404 };\n }\n if (targetId && !target) {\n return { ok: false, error: \"target_not_found\", status: 404 };\n }\n\n if (source && source.balance < amount) {\n return { ok: false, error: \"insufficient_funds\", balance: source.balance, status: 409 };\n }\n\n const now = Date.now();\n if (source) {\n query(\n sql(\n `UPDATE ${TABLE_ACCOUNTS}\n SET balance = balance - ?, version = version + 1, updated_at = ?\n WHERE player_id = ? AND balance >= ?`,\n [amount, now, source.player_id, amount]\n )\n );\n }\n if (target) {\n query(\n sql(\n `UPDATE ${TABLE_ACCOUNTS}\n SET balance = balance + ?, version = version + 1, updated_at = ?\n WHERE player_id = ?`,\n [amount, now, target.player_id]\n )\n );\n }\n\n const transactionType = String(data.transaction_type ?? data.type ?? \"adjustment\");\n const referenceType = String(data.reference_type ?? data.referenceType ?? \"\");\n const referenceId = String(data.reference_id ?? data.referenceId ?? \"\");\n const reason = String(data.reason ?? data.reasonAlias ?? \"\");\n const id = newTransactionId(now);\n\n if (source) {\n query(\n sql(\n `INSERT INTO ${TABLE_TRANSACTIONS}\n (id, transaction_type, actor_id, source_player_id, target_player_id,\n amount, balance_before, balance_after,\n reference_type, reference_id, reason, created_at)\n VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id + \"-dr\",\n transactionType + \".dr\",\n actorId,\n sourceId,\n amount,\n source.balance,\n source.balance - amount,\n referenceType,\n referenceId,\n reason,\n now,\n ]\n )\n );\n }\n if (target) {\n query(\n sql(\n `INSERT INTO ${TABLE_TRANSACTIONS}\n (id, transaction_type, actor_id, source_player_id, target_player_id,\n amount, balance_before, balance_after,\n reference_type, reference_id, reason, created_at)\n VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id + \"-cr\",\n transactionType + \".cr\",\n actorId,\n targetId,\n amount,\n target.balance,\n target.balance + amount,\n referenceType,\n referenceId,\n reason,\n now,\n ]\n )\n );\n }\n\n const sourceView = source ? economyResult(ensureEconomyAccount(query, source.player_id, \"\")) : undefined;\n const targetView = target ? economyResult(ensureEconomyAccount(query, target.player_id, \"\")) : undefined;\n\n const response: ApplyEconomyResult = {\n ok: true,\n transactionId: id,\n source: sourceView\n ? { ...sourceView, balanceBefore: source!.balance, balanceAfter: source!.balance - amount }\n : null,\n target: targetView\n ? { ...targetView, balanceBefore: target!.balance, balanceAfter: target!.balance + amount }\n : null,\n };\n\n if (idempotencyKey) {\n query(\n sql(\n `INSERT INTO ${TABLE_IDEMPOTENCY}\n (actor_id, idempotency_key, transaction_id, response_json, created_at)\n VALUES (?, ?, ?, ?, ?)`,\n [actorId, idempotencyKey, id, JSON.stringify(response), now]\n )\n );\n }\n\n return response;\n}\n\n/**\n * \u72EC\u7ACB\u7ECF\u6D4E\u4E8B\u52A1\u5305\u88C5\u3002alreadyInTx=true \u65F6\u590D\u7528\u5916\u5C42\u4E8B\u52A1(\u4E0D\u518D BEGIN)\u3002\n */\nexport function applyEconomyTransaction(\n query: AnyQuery,\n db: DatabaseSync,\n data: ApplyEconomyInput,\n opts?: { alreadyInTx?: boolean }\n): ApplyEconomyResult {\n if (opts?.alreadyInTx) {\n return applyEconomySteps(query, data);\n }\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = applyEconomySteps(query, data);\n if (!result.ok) {\n db.exec(\"ROLLBACK\");\n return result;\n }\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n return {\n ok: false,\n error: (error as Error).message || \"economy_transaction_failed\",\n status: 500,\n };\n }\n}\n\n/** \u5217\u51FA\u65E5\u5E38\u4EFB\u52A1(\u9ED8\u8BA4\u4EC5 active \u4E14\u672A\u8FC7\u671F) */\nexport function listDailyTasks(\n query: AnyQuery,\n filter?: { status?: string; includeExpired?: boolean }\n): Array<Record<string, unknown>> {\n const status = filter?.status ?? \"active\";\n const now = Date.now();\n if (filter?.includeExpired) {\n const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ?`, [status]));\n return Array.isArray(rows) ? (rows as Array<Record<string, unknown>>) : [];\n }\n const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ? AND expires_at > ?`, [status, now]));\n return Array.isArray(rows) ? (rows as Array<Record<string, unknown>>) : [];\n}\n\nexport interface SubmitDailyTaskInput {\n actorId: string;\n taskId: string;\n quantity: number;\n}\n\n/** \u65E5\u5E38\u4EFB\u52A1\u63D0\u4EA4\u6838\u5FC3(\u65E0 BEGIN) */\nexport function submitDailyTaskSteps(\n query: AnyQuery,\n data: SubmitDailyTaskInput\n): TxResult<{\n transactionId: string;\n reward: number;\n balance: number;\n balanceVersion: number;\n}> {\n const actorId = data.actorId;\n const taskId = data.taskId;\n const quantity = data.quantity;\n\n const rows = query(\n sql(\n `SELECT * FROM ${TABLE_DAILY}\n WHERE id = ? AND status = 'active' AND expires_at > ?`,\n [taskId, Date.now()]\n )\n ) as Array<Record<string, unknown>>;\n const task = rows[0];\n if (!task) {\n return { ok: false, error: \"task_not_found_or_expired\", status: 404 };\n }\n const targetQty = Number(task.target_qty ?? 0);\n const filledQty = Number(task.filled_qty ?? 0);\n const remaining = targetQty - filledQty;\n if (remaining < quantity) {\n return { ok: false, error: \"task_quota_exceeded\", status: 409, extra: { remaining } };\n }\n const reward = Number(task.unit_reward) * quantity;\n\n query(sql(`UPDATE ${TABLE_DAILY} SET filled_qty = filled_qty + ? WHERE id = ?`, [quantity, taskId]));\n\n const result = applyEconomySteps(query, {\n actor_id: actorId,\n transaction_type: \"daily_task.reward\",\n target_player_id: actorId,\n amount: reward,\n reference_type: \"daily_task\",\n reference_id: taskId,\n reason: `\u63D0\u4EA4\u4EFB\u52A1 ${task.item_type}*${quantity}`,\n });\n if (!result.ok) {\n return { ok: false, error: result.error, status: result.status };\n }\n const tgtFinal = economyResult(ensureEconomyAccount(query, actorId, \"\"));\n return {\n ok: true,\n data: {\n transactionId: result.transactionId,\n reward,\n balance: tgtFinal?.balance ?? 0,\n balanceVersion: tgtFinal?.version ?? 0,\n },\n };\n}\n\n/** \u65E5\u5E38\u4EFB\u52A1\u63D0\u4EA4\u5305\u88C5(\u53EF\u72EC\u7ACB\u4E8B\u52A1\u6216\u5D4C\u5165\u5916\u5C42 tx) */\nexport function submitDailyTaskTx(\n query: AnyQuery,\n db: DatabaseSync,\n data: SubmitDailyTaskInput,\n opts?: { alreadyInTx?: boolean }\n): TxResult<{\n transactionId: string;\n reward: number;\n balance: number;\n balanceVersion: number;\n}> {\n if (opts?.alreadyInTx) {\n return submitDailyTaskSteps(query, data);\n }\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = submitDailyTaskSteps(query, data);\n if (!result.ok) {\n db.exec(\"ROLLBACK\");\n return result;\n }\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n return {\n ok: false,\n error: (error as Error).message || \"task_submit_failed\",\n status: 500,\n };\n }\n}\n\n/** \u6708\u5EA6/\u5168\u5C40\u7ECF\u6D4E\u767D\u76AE\u4E66(\u4F18\u5148\u8BFB sfmc_economy_stats,\u5426\u5219\u73B0\u573A\u805A\u5408) */\nexport function monthlyEconomyStats(query: AnyQuery): {\n id: string;\n total_issued: number;\n total_destroyed: number;\n total_supply: number;\n active_accounts: number;\n} {\n const now = new Date();\n const id = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n\n const cached = query(sql(`SELECT * FROM ${TABLE_STATS} WHERE id = ? OR id = ?`, [\"global\", id]));\n if (Array.isArray(cached) && cached.length > 0) {\n const row = cached[0] as Record<string, unknown>;\n return {\n id: String(row.id ?? id),\n total_issued: Number(row.total_issued ?? 0),\n total_destroyed: Number(row.total_destroyed ?? 0),\n total_supply: Number(row.total_supply ?? 0),\n active_accounts: Number(row.active_accounts ?? 0),\n };\n }\n\n const supplyRows = query(\n sql(`SELECT COALESCE(SUM(balance),0) AS total_supply, COUNT(*) AS active_accounts FROM ${TABLE_ACCOUNTS}`)\n );\n const supply = Array.isArray(supplyRows) ? (supplyRows[0] as Record<string, unknown>) : {};\n const issuedRows = query(\n sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE target_player_id IS NOT NULL`)\n );\n const destroyedRows = query(\n sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE source_player_id IS NOT NULL`)\n );\n const issued = Array.isArray(issuedRows) ? Number((issuedRows[0] as Record<string, unknown>).total ?? 0) : 0;\n const destroyed = Array.isArray(destroyedRows) ? Number((destroyedRows[0] as Record<string, unknown>).total ?? 0) : 0;\n\n return {\n id,\n total_issued: issued,\n total_destroyed: destroyed,\n total_supply: Number(supply.total_supply ?? 0),\n active_accounts: Number(supply.active_accounts ?? 0),\n };\n}\n\n"],
|
|
5
|
-
"mappings": "AAgBA,SAAS,6BAA6B;AACtC,SAAS,WAAW;AAmDpB,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAC3B,MAAM,oBAAoB;AAC1B,MAAM,cAAc;AACpB,MAAM,cAAc;AAQpB,SAAS,iBAAiB,KAAqB;AAC7C,SAAO,IAAI,IAAI,SAAS,EAAE,CAAC,GAAG,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AACtE;
|
|
4
|
+
"sourcesContent": ["/**\n * domain/economy.ts \u2014 \u7ECF\u6D4E\u7CFB\u7EDF\u4E1A\u52A1\u6838\u5FC3\n *\n * \u63D0\u4F9B\u8D26\u6237 upsert\u3001\u901A\u7528\u8F6C\u8D26\u3001\u65E5\u5E38\u4EFB\u52A1\u5956\u52B1\u4E0E\u6708\u5EA6\u7EDF\u8BA1\u3002\n * land / chat / gui \u7B49\u901A\u8FC7 service(economy.account.*) \u8C03\u7528\u672C\u6587\u4EF6\u3002\n *\n * \u4E8B\u52A1\u7EA6\u5B9A(\u65B9\u6848 A):\n * - applyEconomySteps / submitDailyTaskSteps \u2014 \u65E0 BEGIN/COMMIT,\u53EF\u5D4C\u8FDB\u5916\u5C42 db.tx\n * - applyEconomyTransaction / submitDailyTaskTx \u2014 \u72EC\u7ACB\u8C03\u7528\u65F6\u81EA\u5DF1\u5F00\u4E8B\u52A1;\n * alreadyInTx=true \u65F6\u53EA\u8DD1 steps\n *\n * \u884C\u7C7B\u578B\u5728\u6B64\u672C\u5730\u5B9A\u4E49\uFF08\u5E73\u53F0\u4E0D\u4F9D\u8D56\u4E1A\u52A1\u6A21\u5757\u5305\uFF1B\u6743\u5A01\u526F\u672C\u5728 sfmc-modules/packages/economy\uFF09\u3002\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\nimport type { SQLStatement } from \"sql-template-strings\";\nimport { isValidIdempotencyKey } from \"../lib/idempotency.js\";\nimport { sql } from \"../lib/sql-helpers.js\";\nimport type { TxResult } from \"./transaction.js\";\nexport type { TxResult };\n\n/**\n * \u7ECF\u6D4E\u57DF\u884C\u6A21\u578B\uFF08\u539F SDK contracts/economy\uFF1B\u5951\u7EA6\u5185\u805A\u5230\u8868\u6240\u6709\u8005 db-server\uFF09\u3002\n * \u4E1A\u52A1\u6A21\u5757\u5E94\u901A\u8FC7 service(economy.*) \u6D88\u8D39\u89C6\u56FE\uFF0C\u4E0D\u4F9D\u8D56\u8FD9\u4E9B\u5185\u90E8\u884C\u7C7B\u578B\u3002\n */\nexport interface EconomyAccountRow {\n player_id: string;\n player_name_snapshot: string;\n balance: number;\n version: number;\n created_at: number;\n updated_at: number;\n}\n\n/** \u5E42\u7B49\u952E\u8868\u884C */\nexport interface EconomyIdempotencyRow {\n actor_id: string;\n idempotency_key: string;\n transaction_id: string;\n response_json: string;\n created_at: number;\n}\n\n/** \u6D41\u6C34\u8868\u884C */\nexport interface EconomyTransactionRow {\n id: string;\n transaction_type: string;\n actor_id: string;\n source_player_id?: string;\n target_player_id?: string;\n amount: number;\n balance_before?: number;\n balance_after?: number;\n reference_type: string;\n reference_id: string;\n reason: string;\n created_at: number;\n idempotency_key: string;\n\n /** \u517C\u5BB9 JS \u9886\u57DF\u6A21\u578B\u8BBF\u95EE\u7684\u9A7C\u5CF0\u522B\u540D\u5B57\u6BB5\u3002 */\n actorId?: string;\n type?: string;\n referenceType?: string;\n referenceId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n}\n\nconst TABLE_ACCOUNTS = \"sfmc_economy_accounts\";\nconst TABLE_TRANSACTIONS = \"sfmc_economy_transactions\";\nconst TABLE_IDEMPOTENCY = \"sfmc_economy_idempotency\";\nconst TABLE_DAILY = \"sfmc_economy_daily_tasks\";\nconst TABLE_STATS = \"sfmc_economy_stats\";\n\n/** query \u517C\u5BB9\u683C\u5F0F\uFF1Astring / SQLStatement / { sql, values } */\nexport type AnyQuery = (\n sql: string | SQLStatement | { sql: string; values?: unknown[] },\n values?: unknown[]\n) => unknown[] | { changes: number | bigint };\n\nfunction newTransactionId(now: number): string {\n return `E${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * \u7ECF\u6D4E\u8D26\u6237 DB \u884C\uFF08snake_case\uFF0C\u5BF9\u5E94 sfmc_economy_accounts\uFF09\u3002\n * \u4E1A\u52A1\u57DF\u7C7B\u578B\u7531\u672C\u6A21\u5757\u7EF4\u62A4\uFF0C\u4E0D\u518D\u4F9D\u8D56 sdk/contracts\u3002\n */\nexport interface EconomyAccountRow {\n player_id: string;\n player_name_snapshot: string;\n balance: number;\n version: number;\n created_at: number;\n updated_at: number;\n}\n\n/**\n * \u7ECF\u6D4E\u4EA4\u6613\u6D41\u6C34 DB \u884C\uFF08snake_case\uFF0C\u5BF9\u5E94 sfmc_economy_transactions\uFF09\u3002\n * \u542B\u5386\u53F2 JS \u4FA7\u522B\u540D\u5B57\u6BB5\uFF0C\u4F9B\u8C03\u7528\u65B9\u517C\u5BB9\u8BFB\u53D6\u3002\n */\nexport interface EconomyTransactionRow {\n id: string;\n transaction_type: string;\n actor_id: string;\n source_player_id?: string;\n target_player_id?: string;\n amount: number;\n balance_before?: number;\n balance_after?: number;\n reference_type: string;\n reference_id: string;\n reason: string;\n created_at: number;\n idempotency_key: string;\n\n /** \u517C\u5BB9 JS \u9886\u57DF\u6A21\u578B\u8BBF\u95EE\u7684\u9A7C\u5CF0\u522B\u540D\u5B57\u6BB5\u3002 */\n actorId?: string;\n type?: string;\n referenceType?: string;\n referenceId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n}\n\n\nexport interface EconomyAccountView {\n playerId: string;\n playerName: string;\n balance: number;\n version: number;\n}\n\nexport function economyResult(row: EconomyAccountRow | undefined): EconomyAccountView | undefined {\n if (!row) return undefined;\n return {\n playerId: row.player_id,\n playerName: row.player_name_snapshot,\n balance: row.balance,\n version: row.version,\n };\n}\n\n/** \u786E\u4FDD\u8D26\u6237\u5B58\u5728\u5E76\u8FD4\u56DE\u6700\u65B0\u884C */\nexport function ensureEconomyAccount(\n query: AnyQuery,\n playerId: string,\n playerName: string\n): EconomyAccountRow | undefined {\n const now = Date.now();\n query(\n sql(\n `INSERT INTO ${TABLE_ACCOUNTS} (player_id, player_name_snapshot, balance, version, created_at, updated_at)\n VALUES (?, ?, 0, 1, ?, ?)\n ON CONFLICT(player_id) DO UPDATE SET\n player_name_snapshot = excluded.player_name_snapshot,\n updated_at = excluded.updated_at`,\n [String(playerId), String(playerName), now, now]\n )\n );\n const rows = query(sql(`SELECT * FROM ${TABLE_ACCOUNTS} WHERE player_id = ?`, [String(playerId)]));\n if (Array.isArray(rows)) {\n return (rows as EconomyAccountRow[])[0];\n }\n return undefined;\n}\n\n/** \u8BFB\u53D6(\u5FC5\u8981\u65F6\u521B\u5EFA)\u8D26\u6237\u4E1A\u52A1\u89C6\u56FE */\nexport function getEconomyAccount(query: AnyQuery, playerId: string, playerName = \"\"): EconomyAccountView | null {\n const row = ensureEconomyAccount(query, playerId, playerName);\n return economyResult(row) ?? null;\n}\n\nexport interface ApplyEconomyInput {\n actor_id?: string;\n amount: number;\n transaction_type?: string;\n source_player_id?: string;\n target_player_id?: string;\n reference_type?: string;\n reference_id?: string;\n reason?: string;\n idempotency_key?: string;\n\n actorId?: string;\n type?: string;\n sourcePlayerId?: string;\n targetPlayerId?: string;\n sourcePlayerName?: string;\n targetPlayerName?: string;\n referenceType?: string;\n referenceId?: string;\n reasonAlias?: string;\n idempotencyKey?: string;\n}\n\nexport type ApplyEconomyResult =\n | {\n ok: true;\n transactionId: string;\n replayed?: boolean;\n source:\n | (EconomyAccountView & {\n balanceBefore: number;\n balanceAfter: number;\n })\n | null;\n target:\n | (EconomyAccountView & {\n balanceBefore: number;\n balanceAfter: number;\n })\n | null;\n }\n | {\n ok: false;\n error: string;\n status: number;\n balance?: number;\n };\n\n/**\n * \u7ECF\u6D4E\u53D8\u66F4\u6838\u5FC3(\u65E0 BEGIN/COMMIT)\u3002\n * \u7EAF debit(\u4EC5 source)\u6216\u7EAF credit(\u4EC5 target)\u5747\u53EF\u6210\u529F\u3002\n */\nexport function applyEconomySteps(query: AnyQuery, data: ApplyEconomyInput): ApplyEconomyResult {\n const amount = Number(data.amount);\n const actorId = String(data.actor_id ?? data.actorId ?? \"\").trim();\n if (!actorId || !Number.isSafeInteger(amount) || amount <= 0) {\n return { ok: false, error: \"invalid_amount\", status: 400 };\n }\n\n const sourceId = data.source_player_id\n ? String(data.source_player_id)\n : data.sourcePlayerId\n ? String(data.sourcePlayerId)\n : null;\n const targetId = data.target_player_id\n ? String(data.target_player_id)\n : data.targetPlayerId\n ? String(data.targetPlayerId)\n : null;\n\n const idempotencyKey = String(data.idempotency_key ?? data.idempotencyKey ?? \"\").trim();\n if (idempotencyKey && !isValidIdempotencyKey(idempotencyKey)) {\n return { ok: false, error: \"invalid_idempotency_key\", status: 400 };\n }\n if (!sourceId && !targetId) {\n return { ok: false, error: \"missing_account\", status: 400 };\n }\n if (sourceId && sourceId !== actorId) {\n return { ok: false, error: \"forbidden_source\", status: 403 };\n }\n if (sourceId && targetId && sourceId === targetId) {\n return { ok: false, error: \"same_account\", status: 400 };\n }\n\n // \u5E42\u7B49\u56DE\u653E\n if (idempotencyKey) {\n const rows = query(\n sql(\n `SELECT response_json FROM ${TABLE_IDEMPOTENCY}\n WHERE actor_id = ? AND idempotency_key = ?`,\n [actorId, idempotencyKey]\n )\n );\n const previous = Array.isArray(rows) ? (rows as Array<{ response_json: string }>)[0] : undefined;\n if (previous) {\n return { ...JSON.parse(previous.response_json), replayed: true } as ApplyEconomyResult;\n }\n }\n\n const source = sourceId ? ensureEconomyAccount(query, sourceId, String(data.sourcePlayerName ?? \"\")) : null;\n const target = targetId ? ensureEconomyAccount(query, targetId, String(data.targetPlayerName ?? \"\")) : null;\n if (sourceId && !source) {\n return { ok: false, error: \"source_not_found\", status: 404 };\n }\n if (targetId && !target) {\n return { ok: false, error: \"target_not_found\", status: 404 };\n }\n\n if (source && source.balance < amount) {\n return { ok: false, error: \"insufficient_funds\", balance: source.balance, status: 409 };\n }\n\n const now = Date.now();\n if (source) {\n query(\n sql(\n `UPDATE ${TABLE_ACCOUNTS}\n SET balance = balance - ?, version = version + 1, updated_at = ?\n WHERE player_id = ? AND balance >= ?`,\n [amount, now, source.player_id, amount]\n )\n );\n }\n if (target) {\n query(\n sql(\n `UPDATE ${TABLE_ACCOUNTS}\n SET balance = balance + ?, version = version + 1, updated_at = ?\n WHERE player_id = ?`,\n [amount, now, target.player_id]\n )\n );\n }\n\n const transactionType = String(data.transaction_type ?? data.type ?? \"adjustment\");\n const referenceType = String(data.reference_type ?? data.referenceType ?? \"\");\n const referenceId = String(data.reference_id ?? data.referenceId ?? \"\");\n const reason = String(data.reason ?? data.reasonAlias ?? \"\");\n const id = newTransactionId(now);\n\n if (source) {\n query(\n sql(\n `INSERT INTO ${TABLE_TRANSACTIONS}\n (id, transaction_type, actor_id, source_player_id, target_player_id,\n amount, balance_before, balance_after,\n reference_type, reference_id, reason, created_at)\n VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id + \"-dr\",\n transactionType + \".dr\",\n actorId,\n sourceId,\n amount,\n source.balance,\n source.balance - amount,\n referenceType,\n referenceId,\n reason,\n now,\n ]\n )\n );\n }\n if (target) {\n query(\n sql(\n `INSERT INTO ${TABLE_TRANSACTIONS}\n (id, transaction_type, actor_id, source_player_id, target_player_id,\n amount, balance_before, balance_after,\n reference_type, reference_id, reason, created_at)\n VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`,\n [\n id + \"-cr\",\n transactionType + \".cr\",\n actorId,\n targetId,\n amount,\n target.balance,\n target.balance + amount,\n referenceType,\n referenceId,\n reason,\n now,\n ]\n )\n );\n }\n\n const sourceView = source ? economyResult(ensureEconomyAccount(query, source.player_id, \"\")) : undefined;\n const targetView = target ? economyResult(ensureEconomyAccount(query, target.player_id, \"\")) : undefined;\n\n const response: ApplyEconomyResult = {\n ok: true,\n transactionId: id,\n source: sourceView\n ? { ...sourceView, balanceBefore: source!.balance, balanceAfter: source!.balance - amount }\n : null,\n target: targetView\n ? { ...targetView, balanceBefore: target!.balance, balanceAfter: target!.balance + amount }\n : null,\n };\n\n if (idempotencyKey) {\n query(\n sql(\n `INSERT INTO ${TABLE_IDEMPOTENCY}\n (actor_id, idempotency_key, transaction_id, response_json, created_at)\n VALUES (?, ?, ?, ?, ?)`,\n [actorId, idempotencyKey, id, JSON.stringify(response), now]\n )\n );\n }\n\n return response;\n}\n\n/**\n * \u72EC\u7ACB\u7ECF\u6D4E\u4E8B\u52A1\u5305\u88C5\u3002\u82E5\u6307\u5B9A `alreadyInTx: true` \u5219\u590D\u7528\u5916\u5C42\u4E8B\u52A1\uFF08\u4E0D\u518D\u5355\u72EC\u5F00\u542F BEGIN\uFF09\u3002\n *\n * @param query \u6570\u636E\u5E93\u67E5\u8BE2\u6267\u884C\u51FD\u6570\u3002\n * @param db \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n * @param data \u7ECF\u6D4E\u64CD\u4F5C\u8F93\u5165\u53C2\u6570\u3002\n * @param opts \u53EF\u9009\u4E8B\u52A1\u53C2\u6570\u3002\n * @returns \u7ECF\u6D4E\u53D8\u52A8\u7ED3\u679C\u3002\n */\nexport function applyEconomyTransaction(\n query: AnyQuery,\n db: DatabaseSync,\n data: ApplyEconomyInput,\n opts?: { alreadyInTx?: boolean }\n): ApplyEconomyResult {\n if (opts?.alreadyInTx) {\n return applyEconomySteps(query, data);\n }\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = applyEconomySteps(query, data);\n if (!result.ok) {\n db.exec(\"ROLLBACK\");\n return result;\n }\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n return {\n ok: false,\n error: (error as Error).message || \"economy_transaction_failed\",\n status: 500,\n };\n }\n}\n\n/**\n * \u5217\u51FA\u65E5\u5E38\u4EFB\u52A1\u5217\u8868\uFF08\u9ED8\u8BA4\u4EC5\u67E5\u8BE2 active \u72B6\u6001\u4E14\u5C1A\u672A\u8FC7\u671F\u7684\u4EFB\u52A1\uFF09\u3002\n *\n * @param query \u6570\u636E\u5E93\u67E5\u8BE2\u6267\u884C\u51FD\u6570\u3002\n * @param filter \u8FC7\u6EE4\u6761\u4EF6\uFF08\u53EF\u6307\u5B9A\u72B6\u6001\u6216\u662F\u5426\u5305\u542B\u5DF2\u8FC7\u671F\u4EFB\u52A1\uFF09\u3002\n * @returns \u4EFB\u52A1\u884C\u8BB0\u5F55\u6570\u7EC4\u3002\n */\nexport function listDailyTasks(\n query: AnyQuery,\n filter?: { status?: string; includeExpired?: boolean }\n): Array<Record<string, unknown>> {\n const status = filter?.status ?? \"active\";\n const now = Date.now();\n if (filter?.includeExpired) {\n const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ?`, [status]));\n return Array.isArray(rows) ? (rows as Array<Record<string, unknown>>) : [];\n }\n const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ? AND expires_at > ?`, [status, now]));\n return Array.isArray(rows) ? (rows as Array<Record<string, unknown>>) : [];\n}\n\nexport interface SubmitDailyTaskInput {\n actorId: string;\n taskId: string;\n quantity: number;\n}\n\n/**\n * \u65E5\u5E38\u4EFB\u52A1\u63D0\u4EA4\u6838\u5FC3\u903B\u8F91\uFF08\u65E0\u663E\u5F0F\u4E8B\u52A1\u63A7\u5236\uFF0C\u4F9B\u5185\u90E8\u6216\u5916\u5C42\u4E8B\u52A1\u8C03\u7528\uFF09\u3002\n *\n * @param query \u6570\u636E\u5E93\u67E5\u8BE2\u6267\u884C\u51FD\u6570\u3002\n * @param data \u4EFB\u52A1\u63D0\u4EA4\u6570\u636E\uFF08\u73A9\u5BB6 ID\u3001\u4EFB\u52A1 ID\u3001\u63D0\u4EA4\u6570\u91CF\uFF09\u3002\n * @returns \u63D0\u4EA4\u7ED3\u679C\uFF08\u542B\u5956\u52B1\u91D1\u989D\u3001\u66F4\u65B0\u540E\u7684\u4F59\u989D\u53CA\u7248\u672C\u53F7\uFF09\u3002\n */\nexport function submitDailyTaskSteps(\n query: AnyQuery,\n data: SubmitDailyTaskInput\n): TxResult<{\n transactionId: string;\n reward: number;\n balance: number;\n balanceVersion: number;\n}> {\n const actorId = data.actorId;\n const taskId = data.taskId;\n const quantity = data.quantity;\n\n const rows = query(\n sql(\n `SELECT * FROM ${TABLE_DAILY}\n WHERE id = ? AND status = 'active' AND expires_at > ?`,\n [taskId, Date.now()]\n )\n ) as Array<Record<string, unknown>>;\n const task = rows[0];\n if (!task) {\n return { ok: false, error: \"task_not_found_or_expired\", status: 404 };\n }\n const targetQty = Number(task.target_qty ?? 0);\n const filledQty = Number(task.filled_qty ?? 0);\n const remaining = targetQty - filledQty;\n if (remaining < quantity) {\n return { ok: false, error: \"task_quota_exceeded\", status: 409, extra: { remaining } };\n }\n const reward = Number(task.unit_reward) * quantity;\n\n query(sql(`UPDATE ${TABLE_DAILY} SET filled_qty = filled_qty + ? WHERE id = ?`, [quantity, taskId]));\n\n const result = applyEconomySteps(query, {\n actor_id: actorId,\n transaction_type: \"daily_task.reward\",\n target_player_id: actorId,\n amount: reward,\n reference_type: \"daily_task\",\n reference_id: taskId,\n reason: `\u63D0\u4EA4\u4EFB\u52A1 ${task.item_type}*${quantity}`,\n });\n if (!result.ok) {\n return { ok: false, error: result.error, status: result.status };\n }\n const tgtFinal = economyResult(ensureEconomyAccount(query, actorId, \"\"));\n return {\n ok: true,\n data: {\n transactionId: result.transactionId,\n reward,\n balance: tgtFinal?.balance ?? 0,\n balanceVersion: tgtFinal?.version ?? 0,\n },\n };\n}\n\n/**\n * \u65E5\u5E38\u4EFB\u52A1\u63D0\u4EA4\u4E8B\u52A1\u5305\u88C5\uFF08\u53EF\u4F5C\u4E3A\u72EC\u7ACB IMMEDIATE \u4E8B\u52A1\u8FD0\u884C\uFF0C\u6216\u5D4C\u5165\u5916\u5C42\u73B0\u6709\u4E8B\u52A1\uFF09\u3002\n *\n * @param query \u6570\u636E\u5E93\u67E5\u8BE2\u6267\u884C\u51FD\u6570\u3002\n * @param db \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n * @param data \u4EFB\u52A1\u63D0\u4EA4\u6570\u636E\u3002\n * @param opts \u53EF\u9009\u914D\u7F6E\u9879\u3002\n * @returns \u4EFB\u52A1\u63D0\u4EA4\u4E8B\u52A1\u7ED3\u679C\u3002\n */\nexport function submitDailyTaskTx(\n query: AnyQuery,\n db: DatabaseSync,\n data: SubmitDailyTaskInput,\n opts?: { alreadyInTx?: boolean }\n): TxResult<{\n transactionId: string;\n reward: number;\n balance: number;\n balanceVersion: number;\n}> {\n if (opts?.alreadyInTx) {\n return submitDailyTaskSteps(query, data);\n }\n db.exec(\"BEGIN IMMEDIATE\");\n try {\n const result = submitDailyTaskSteps(query, data);\n if (!result.ok) {\n db.exec(\"ROLLBACK\");\n return result;\n }\n db.exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n db.exec(\"ROLLBACK\");\n } catch {\n /* ignore */\n }\n return {\n ok: false,\n error: (error as Error).message || \"task_submit_failed\",\n status: 500,\n };\n }\n}\n\n/**\n * \u83B7\u53D6\u6708\u5EA6\u6216\u5168\u5C40\u7ECF\u6D4E\u7EDF\u8BA1\u6307\u6807\u767D\u76AE\u4E66\uFF08\u4F18\u5148\u8BFB\u53D6 `sfmc_economy_stats` \u7EDF\u8BA1\u8868\uFF0C\u65E0\u7F13\u5B58\u65F6\u73B0\u573A\u805A\u5408\u8BA1\u7B97\uFF09\u3002\n *\n * @param query \u6570\u636E\u5E93\u67E5\u8BE2\u6267\u884C\u51FD\u6570\u3002\n * @returns \u5305\u542B\u53D1\u884C\u603B\u91CF\u3001\u9500\u6BC1\u603B\u91CF\u3001\u73B0\u5B58\u603B\u91CF\u4E0E\u6D3B\u8DC3\u8D26\u6237\u6570\u7684\u7EDF\u8BA1\u5FEB\u7167\u3002\n */\nexport function monthlyEconomyStats(query: AnyQuery): {\n id: string;\n total_issued: number;\n total_destroyed: number;\n total_supply: number;\n active_accounts: number;\n} {\n\n const now = new Date();\n const id = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, \"0\")}`;\n\n const cached = query(sql(`SELECT * FROM ${TABLE_STATS} WHERE id = ? OR id = ?`, [\"global\", id]));\n if (Array.isArray(cached) && cached.length > 0) {\n const row = cached[0] as Record<string, unknown>;\n return {\n id: String(row.id ?? id),\n total_issued: Number(row.total_issued ?? 0),\n total_destroyed: Number(row.total_destroyed ?? 0),\n total_supply: Number(row.total_supply ?? 0),\n active_accounts: Number(row.active_accounts ?? 0),\n };\n }\n\n const supplyRows = query(\n sql(`SELECT COALESCE(SUM(balance),0) AS total_supply, COUNT(*) AS active_accounts FROM ${TABLE_ACCOUNTS}`)\n );\n const supply = Array.isArray(supplyRows) ? (supplyRows[0] as Record<string, unknown>) : {};\n const issuedRows = query(\n sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE target_player_id IS NOT NULL`)\n );\n const destroyedRows = query(\n sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE source_player_id IS NOT NULL`)\n );\n const issued = Array.isArray(issuedRows) ? Number((issuedRows[0] as Record<string, unknown>).total ?? 0) : 0;\n const destroyed = Array.isArray(destroyedRows) ? Number((destroyedRows[0] as Record<string, unknown>).total ?? 0) : 0;\n\n return {\n id,\n total_issued: issued,\n total_destroyed: destroyed,\n total_supply: Number(supply.total_supply ?? 0),\n active_accounts: Number(supply.active_accounts ?? 0),\n };\n}\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["rows"]
|
|
7
7
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages-bridge-filter.test.d.ts","sourceRoot":"","sources":["../../src/domain/messages-bridge-filter.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { shouldForwardChatToQQ } from "../routes/messages.js";
|
|
4
|
+
test("shouldForwardChatToQQ\uFF1A\u672A\u914D bridge \u4E0D\u51FA\u7AD9", () => {
|
|
5
|
+
assert.equal(shouldForwardChatToQQ("", "bridge", "xuid"), false);
|
|
6
|
+
});
|
|
7
|
+
test("shouldForwardChatToQQ\uFF1A\u9891\u9053\u4E0D\u5339\u914D\u4E0D\u51FA\u7AD9", () => {
|
|
8
|
+
assert.equal(shouldForwardChatToQQ("bridge", "other", "xuid"), false);
|
|
9
|
+
});
|
|
10
|
+
test("shouldForwardChatToQQ\uFF1Aqq_ \u6765\u6E90\u4E0D\u51FA\u7AD9\uFF08\u9632\u56DE\u73AF\uFF09", () => {
|
|
11
|
+
assert.equal(shouldForwardChatToQQ("bridge", "bridge", "qq_123"), false);
|
|
12
|
+
});
|
|
13
|
+
test("shouldForwardChatToQQ\uFF1A\u7A7A fromId \u4E0D\u51FA\u7AD9", () => {
|
|
14
|
+
assert.equal(shouldForwardChatToQQ("bridge", "bridge", ""), false);
|
|
15
|
+
assert.equal(shouldForwardChatToQQ("bridge", "bridge", " "), false);
|
|
16
|
+
});
|
|
17
|
+
test("shouldForwardChatToQQ\uFF1A\u5339\u914D\u9891\u9053\u7684\u73A9\u5BB6\u6D88\u606F\u51FA\u7AD9", () => {
|
|
18
|
+
assert.equal(shouldForwardChatToQQ("bridge", "bridge", "player-1"), true);
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=messages-bridge-filter.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/domain/messages-bridge-filter.test.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * messages \u51FA\u7AD9\u8FC7\u6EE4\u5355\u6D4B\n */\n\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport { shouldForwardChatToQQ } from \"../routes/messages.js\";\n\ntest(\"shouldForwardChatToQQ\uFF1A\u672A\u914D bridge \u4E0D\u51FA\u7AD9\", () => {\n assert.equal(shouldForwardChatToQQ(\"\", \"bridge\", \"xuid\"), false);\n});\n\ntest(\"shouldForwardChatToQQ\uFF1A\u9891\u9053\u4E0D\u5339\u914D\u4E0D\u51FA\u7AD9\", () => {\n assert.equal(shouldForwardChatToQQ(\"bridge\", \"other\", \"xuid\"), false);\n});\n\ntest(\"shouldForwardChatToQQ\uFF1Aqq_ \u6765\u6E90\u4E0D\u51FA\u7AD9\uFF08\u9632\u56DE\u73AF\uFF09\", () => {\n assert.equal(shouldForwardChatToQQ(\"bridge\", \"bridge\", \"qq_123\"), false);\n});\n\ntest(\"shouldForwardChatToQQ\uFF1A\u7A7A fromId \u4E0D\u51FA\u7AD9\", () => {\n assert.equal(shouldForwardChatToQQ(\"bridge\", \"bridge\", \"\"), false);\n assert.equal(shouldForwardChatToQQ(\"bridge\", \"bridge\", \" \"), false);\n});\n\ntest(\"shouldForwardChatToQQ\uFF1A\u5339\u914D\u9891\u9053\u7684\u73A9\u5BB6\u6D88\u606F\u51FA\u7AD9\", () => {\n assert.equal(shouldForwardChatToQQ(\"bridge\", \"bridge\", \"player-1\"), true);\n});\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,YAAY;AACnB,OAAO,UAAU;AACjB,SAAS,6BAA6B;AAEtC,KAAK,qEAAuC,MAAM;AAChD,SAAO,MAAM,sBAAsB,IAAI,UAAU,MAAM,GAAG,KAAK;AACjE,CAAC;AAED,KAAK,+EAAkC,MAAM;AAC3C,SAAO,MAAM,sBAAsB,UAAU,SAAS,MAAM,GAAG,KAAK;AACtE,CAAC;AAED,KAAK,+FAAwC,MAAM;AACjD,SAAO,MAAM,sBAAsB,UAAU,UAAU,QAAQ,GAAG,KAAK;AACzE,CAAC;AAED,KAAK,+DAAsC,MAAM;AAC/C,SAAO,MAAM,sBAAsB,UAAU,UAAU,EAAE,GAAG,KAAK;AACjE,SAAO,MAAM,sBAAsB,UAAU,UAAU,KAAK,GAAG,KAAK;AACtE,CAAC;AAED,KAAK,iGAAqC,MAAM;AAC9C,SAAO,MAAM,sBAAsB,UAAU,UAAU,UAAU,GAAG,IAAI;AAC1E,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qq-bind-status.test.d.ts","sourceRoot":"","sources":["../../src/domain/qq-bind-status.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { SQL } from "sql-template-strings";
|
|
5
|
+
import { initSchema } from "./schema.js";
|
|
6
|
+
import { createQuery } from "../lib/sqlite.js";
|
|
7
|
+
import { createStatusRoutes, FRESH_MS } from "../routes/status.js";
|
|
8
|
+
import { createQqBindRoutes } from "../routes/qq-bind.js";
|
|
9
|
+
function memDb() {
|
|
10
|
+
const db = new DatabaseSync(":memory:");
|
|
11
|
+
initSchema(db);
|
|
12
|
+
return { db, query: createQuery(db) };
|
|
13
|
+
}
|
|
14
|
+
function mockRes() {
|
|
15
|
+
let status = 200;
|
|
16
|
+
let body = {};
|
|
17
|
+
return {
|
|
18
|
+
res: {
|
|
19
|
+
writeHead(code) {
|
|
20
|
+
status = code;
|
|
21
|
+
},
|
|
22
|
+
end(s) {
|
|
23
|
+
body = JSON.parse(s);
|
|
24
|
+
},
|
|
25
|
+
setHeader() {
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
get status() {
|
|
29
|
+
return status;
|
|
30
|
+
},
|
|
31
|
+
get body() {
|
|
32
|
+
return body;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
test("initSchema \u542B qq bind \u8868", () => {
|
|
37
|
+
const { db } = memDb();
|
|
38
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'sfmc_qq_%' ORDER BY name").all();
|
|
39
|
+
const names = tables.map((t) => t.name);
|
|
40
|
+
assert.ok(names.includes("sfmc_qq_bind_pending"));
|
|
41
|
+
assert.ok(names.includes("sfmc_qq_bindings"));
|
|
42
|
+
assert.ok(names.includes("sfmc_qq_join_requests"));
|
|
43
|
+
assert.ok(names.includes("sfmc_qq_admin_actions"));
|
|
44
|
+
assert.ok(FRESH_MS > 0);
|
|
45
|
+
});
|
|
46
|
+
test("GET /api/sfmc/status \u7A7A\u8868\u4E0D\u629B\u9519", async () => {
|
|
47
|
+
const { query } = memDb();
|
|
48
|
+
const handle = createStatusRoutes({
|
|
49
|
+
query,
|
|
50
|
+
collectSystem: async () => ({
|
|
51
|
+
host: {
|
|
52
|
+
hostname: "test-host",
|
|
53
|
+
platform: "win32",
|
|
54
|
+
arch: "x64",
|
|
55
|
+
release: "10.0",
|
|
56
|
+
uptimeSec: 3600,
|
|
57
|
+
uptimeText: "1\u65F60\u5206",
|
|
58
|
+
cpu: { model: "Test CPU", cores: 4 },
|
|
59
|
+
memory: { totalMb: 8192, freeMb: 4096, usedMb: 4096, usedPercent: 50 },
|
|
60
|
+
loadavg: [0, 0, 0]
|
|
61
|
+
},
|
|
62
|
+
db: { pid: 1, running: true, uptimeSec: 10, uptimeText: "10\u79D2" },
|
|
63
|
+
bds: { state: "stopped", running: false, pid: 0, uptimeSec: null, uptimeText: "\u672A\u8FD0\u884C" }
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
const m = mockRes();
|
|
67
|
+
const ok = await handle({
|
|
68
|
+
path: "/api/sfmc/status",
|
|
69
|
+
method: "GET",
|
|
70
|
+
params: new URLSearchParams(),
|
|
71
|
+
req: {},
|
|
72
|
+
res: m.res
|
|
73
|
+
});
|
|
74
|
+
assert.equal(ok, true);
|
|
75
|
+
assert.equal(m.status, 200);
|
|
76
|
+
assert.ok(Array.isArray(m.body.online));
|
|
77
|
+
assert.equal(m.body.online.length, 0);
|
|
78
|
+
assert.equal(m.body.host.hostname, "test-host");
|
|
79
|
+
assert.equal(m.body.processes.bds.state, "stopped");
|
|
80
|
+
});
|
|
81
|
+
test("bind request \u2192 confirm \u2192 me \u2192 unbind + \u51B2\u7A81", async () => {
|
|
82
|
+
const { query } = memDb();
|
|
83
|
+
let bodyData = {};
|
|
84
|
+
const handle = createQqBindRoutes({
|
|
85
|
+
query,
|
|
86
|
+
body: async () => bodyData,
|
|
87
|
+
json: (res, data, status = 200) => {
|
|
88
|
+
res.writeHead(status);
|
|
89
|
+
res.end(JSON.stringify(data));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
bodyData = { openid: "oa", qq_backend: "official" };
|
|
93
|
+
let m = mockRes();
|
|
94
|
+
await handle({
|
|
95
|
+
path: "/api/sfmc/qq/bind/request",
|
|
96
|
+
method: "POST",
|
|
97
|
+
params: new URLSearchParams(),
|
|
98
|
+
req: {},
|
|
99
|
+
res: m.res
|
|
100
|
+
});
|
|
101
|
+
assert.equal(m.body.success, true);
|
|
102
|
+
const code = String(m.body.code);
|
|
103
|
+
assert.match(code, /^\d{6}$/);
|
|
104
|
+
bodyData = { code, xuid: "x1", name: "Steve" };
|
|
105
|
+
m = mockRes();
|
|
106
|
+
await handle({
|
|
107
|
+
path: "/api/sfmc/qq/bind/confirm",
|
|
108
|
+
method: "POST",
|
|
109
|
+
params: new URLSearchParams(),
|
|
110
|
+
req: {},
|
|
111
|
+
res: m.res
|
|
112
|
+
});
|
|
113
|
+
assert.equal(m.body.success, true);
|
|
114
|
+
m = mockRes();
|
|
115
|
+
await handle({
|
|
116
|
+
path: "/api/sfmc/qq/bind/me",
|
|
117
|
+
method: "GET",
|
|
118
|
+
params: new URLSearchParams("openid=oa"),
|
|
119
|
+
req: {},
|
|
120
|
+
res: m.res
|
|
121
|
+
});
|
|
122
|
+
assert.equal(m.body.bound, true);
|
|
123
|
+
assert.equal(m.body.binding.player_name, "Steve");
|
|
124
|
+
bodyData = { openid: "ob", qq_backend: "official" };
|
|
125
|
+
m = mockRes();
|
|
126
|
+
await handle({
|
|
127
|
+
path: "/api/sfmc/qq/bind/request",
|
|
128
|
+
method: "POST",
|
|
129
|
+
params: new URLSearchParams(),
|
|
130
|
+
req: {},
|
|
131
|
+
res: m.res
|
|
132
|
+
});
|
|
133
|
+
const code2 = String(m.body.code);
|
|
134
|
+
bodyData = { code: code2, xuid: "x1", name: "Alex" };
|
|
135
|
+
m = mockRes();
|
|
136
|
+
await handle({
|
|
137
|
+
path: "/api/sfmc/qq/bind/confirm",
|
|
138
|
+
method: "POST",
|
|
139
|
+
params: new URLSearchParams(),
|
|
140
|
+
req: {},
|
|
141
|
+
res: m.res
|
|
142
|
+
});
|
|
143
|
+
assert.equal(m.body.success, false);
|
|
144
|
+
assert.equal(m.body.error, "player_already_bound");
|
|
145
|
+
bodyData = { openid: "oa" };
|
|
146
|
+
m = mockRes();
|
|
147
|
+
await handle({
|
|
148
|
+
path: "/api/sfmc/qq/bind/unbind",
|
|
149
|
+
method: "POST",
|
|
150
|
+
params: new URLSearchParams(),
|
|
151
|
+
req: {},
|
|
152
|
+
res: m.res
|
|
153
|
+
});
|
|
154
|
+
assert.equal(m.body.unbound, true);
|
|
155
|
+
bodyData = { openid: "oa", qq_backend: "official" };
|
|
156
|
+
m = mockRes();
|
|
157
|
+
await handle({
|
|
158
|
+
path: "/api/sfmc/qq/bind/request",
|
|
159
|
+
method: "POST",
|
|
160
|
+
params: new URLSearchParams(),
|
|
161
|
+
req: {},
|
|
162
|
+
res: m.res
|
|
163
|
+
});
|
|
164
|
+
assert.equal(m.body.success, true);
|
|
165
|
+
});
|
|
166
|
+
test("status \u65B0\u9C9C\u73A9\u5BB6\u8BA1\u5165 online", async () => {
|
|
167
|
+
const { query } = memDb();
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
query(
|
|
170
|
+
SQL`INSERT INTO sfmc_players (id, name, updated_at) VALUES ('p1', 'Steve', ${now})`
|
|
171
|
+
);
|
|
172
|
+
query(
|
|
173
|
+
SQL`INSERT INTO sfmc_world (day, difficulty, updated_at) VALUES (3, 'normal', ${String(now)})`
|
|
174
|
+
);
|
|
175
|
+
const handle = createStatusRoutes({
|
|
176
|
+
query,
|
|
177
|
+
collectSystem: async () => ({
|
|
178
|
+
host: {
|
|
179
|
+
hostname: "h",
|
|
180
|
+
platform: "linux",
|
|
181
|
+
arch: "x64",
|
|
182
|
+
release: "6",
|
|
183
|
+
uptimeSec: 1,
|
|
184
|
+
uptimeText: "1\u79D2",
|
|
185
|
+
cpu: { model: "c", cores: 2 },
|
|
186
|
+
memory: { totalMb: 1, freeMb: 1, usedMb: 0, usedPercent: 0 },
|
|
187
|
+
loadavg: [0, 0, 0]
|
|
188
|
+
},
|
|
189
|
+
db: { pid: 1, running: true, uptimeSec: 1, uptimeText: "1\u79D2" },
|
|
190
|
+
bds: {
|
|
191
|
+
state: "running",
|
|
192
|
+
running: true,
|
|
193
|
+
pid: 99,
|
|
194
|
+
uptimeSec: 120,
|
|
195
|
+
uptimeText: "2\u52060\u79D2"
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
const m = mockRes();
|
|
200
|
+
await handle({
|
|
201
|
+
path: "/api/sfmc/status",
|
|
202
|
+
method: "GET",
|
|
203
|
+
params: new URLSearchParams(),
|
|
204
|
+
req: {},
|
|
205
|
+
res: m.res
|
|
206
|
+
});
|
|
207
|
+
assert.equal(m.body.online.length, 1);
|
|
208
|
+
assert.equal(m.body.world.day, 3);
|
|
209
|
+
assert.equal(m.body.processes.bds.pid, 99);
|
|
210
|
+
});
|
|
211
|
+
//# sourceMappingURL=qq-bind-status.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/domain/qq-bind-status.test.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * domain/qq-bind-status.test.ts \u2014 bind \u8868 + status \u67E5\u8BE2\u5F62\u72B6\n */\nimport assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport { DatabaseSync } from \"node:sqlite\";\nimport { SQL } from \"sql-template-strings\";\nimport { initSchema } from \"./schema.js\";\nimport { createQuery } from \"../lib/sqlite.js\";\nimport { createStatusRoutes, FRESH_MS } from \"../routes/status.js\";\nimport { createQqBindRoutes } from \"../routes/qq-bind.js\";\n\nfunction memDb() {\n const db = new DatabaseSync(\":memory:\");\n initSchema(db);\n return { db, query: createQuery(db) };\n}\n\nfunction mockRes() {\n let status = 200;\n let body: Record<string, unknown> = {};\n return {\n res: {\n writeHead(code: number) {\n status = code;\n },\n end(s: string) {\n body = JSON.parse(s) as Record<string, unknown>;\n },\n setHeader() {},\n } as unknown as import(\"http\").ServerResponse,\n get status() {\n return status;\n },\n get body() {\n return body;\n },\n };\n}\n\ntest(\"initSchema \u542B qq bind \u8868\", () => {\n const { db } = memDb();\n const tables = db\n .prepare(\"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'sfmc_qq_%' ORDER BY name\")\n .all() as Array<{ name: string }>;\n const names = tables.map((t) => t.name);\n assert.ok(names.includes(\"sfmc_qq_bind_pending\"));\n assert.ok(names.includes(\"sfmc_qq_bindings\"));\n assert.ok(names.includes(\"sfmc_qq_join_requests\"));\n assert.ok(names.includes(\"sfmc_qq_admin_actions\"));\n assert.ok(FRESH_MS > 0);\n});\n\ntest(\"GET /api/sfmc/status \u7A7A\u8868\u4E0D\u629B\u9519\", async () => {\n const { query } = memDb();\n const handle = createStatusRoutes({\n query,\n collectSystem: async () => ({\n host: {\n hostname: \"test-host\",\n platform: \"win32\",\n arch: \"x64\",\n release: \"10.0\",\n uptimeSec: 3600,\n uptimeText: \"1\u65F60\u5206\",\n cpu: { model: \"Test CPU\", cores: 4 },\n memory: { totalMb: 8192, freeMb: 4096, usedMb: 4096, usedPercent: 50 },\n loadavg: [0, 0, 0],\n },\n db: { pid: 1, running: true, uptimeSec: 10, uptimeText: \"10\u79D2\" },\n bds: { state: \"stopped\", running: false, pid: 0, uptimeSec: null, uptimeText: \"\u672A\u8FD0\u884C\" },\n }),\n });\n const m = mockRes();\n const ok = await handle({\n path: \"/api/sfmc/status\",\n method: \"GET\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(ok, true);\n assert.equal(m.status, 200);\n assert.ok(Array.isArray(m.body.online));\n assert.equal((m.body.online as unknown[]).length, 0);\n assert.equal((m.body.host as { hostname: string }).hostname, \"test-host\");\n assert.equal((m.body.processes as { bds: { state: string } }).bds.state, \"stopped\");\n});\n\ntest(\"bind request \u2192 confirm \u2192 me \u2192 unbind + \u51B2\u7A81\", async () => {\n const { query } = memDb();\n let bodyData: Record<string, unknown> = {};\n const handle = createQqBindRoutes({\n query,\n body: async () => bodyData,\n json: (res, data, status = 200) => {\n (res as unknown as { writeHead: (c: number) => void }).writeHead(status);\n (res as unknown as { end: (s: string) => void }).end(JSON.stringify(data));\n },\n });\n\n bodyData = { openid: \"oa\", qq_backend: \"official\" };\n let m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/request\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(m.body.success, true);\n const code = String(m.body.code);\n assert.match(code, /^\\d{6}$/);\n\n bodyData = { code, xuid: \"x1\", name: \"Steve\" };\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/confirm\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(m.body.success, true);\n\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/me\",\n method: \"GET\",\n params: new URLSearchParams(\"openid=oa\"),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(m.body.bound, true);\n assert.equal((m.body.binding as { player_name: string }).player_name, \"Steve\");\n\n // \u51B2\u7A81\uFF1A\u53E6\u4E00 QQ \u7ED1\u540C\u4E00 xuid\n bodyData = { openid: \"ob\", qq_backend: \"official\" };\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/request\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n const code2 = String(m.body.code);\n bodyData = { code: code2, xuid: \"x1\", name: \"Alex\" };\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/confirm\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(m.body.success, false);\n assert.equal(m.body.error, \"player_already_bound\");\n\n bodyData = { openid: \"oa\" };\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/unbind\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal(m.body.unbound, true);\n\n // \u5DF2\u7ED1\u5B9A\u518D request\n bodyData = { openid: \"oa\", qq_backend: \"official\" };\n m = mockRes();\n await handle({\n path: \"/api/sfmc/qq/bind/request\",\n method: \"POST\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n // \u89E3\u7ED1\u540E\u5E94\u80FD\u518D\u7533\u8BF7\n assert.equal(m.body.success, true);\n});\n\ntest(\"status \u65B0\u9C9C\u73A9\u5BB6\u8BA1\u5165 online\", async () => {\n const { query } = memDb();\n const now = Date.now();\n query(\n SQL`INSERT INTO sfmc_players (id, name, updated_at) VALUES ('p1', 'Steve', ${now})`\n );\n query(\n SQL`INSERT INTO sfmc_world (day, difficulty, updated_at) VALUES (3, 'normal', ${String(now)})`\n );\n const handle = createStatusRoutes({\n query,\n collectSystem: async () => ({\n host: {\n hostname: \"h\",\n platform: \"linux\",\n arch: \"x64\",\n release: \"6\",\n uptimeSec: 1,\n uptimeText: \"1\u79D2\",\n cpu: { model: \"c\", cores: 2 },\n memory: { totalMb: 1, freeMb: 1, usedMb: 0, usedPercent: 0 },\n loadavg: [0, 0, 0],\n },\n db: { pid: 1, running: true, uptimeSec: 1, uptimeText: \"1\u79D2\" },\n bds: {\n state: \"running\",\n running: true,\n pid: 99,\n uptimeSec: 120,\n uptimeText: \"2\u52060\u79D2\",\n },\n }),\n });\n const m = mockRes();\n await handle({\n path: \"/api/sfmc/status\",\n method: \"GET\",\n params: new URLSearchParams(),\n req: {} as import(\"http\").IncomingMessage,\n res: m.res,\n });\n assert.equal((m.body.online as unknown[]).length, 1);\n assert.equal((m.body.world as { day: number }).day, 3);\n assert.equal((m.body.processes as { bds: { pid: number } }).bds.pid, 99);\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* domain/qq-events.ts — Minecraft 服务器事件群推送聚合节流器
|
|
3
|
+
*
|
|
4
|
+
* 聚合策略:
|
|
5
|
+
* - 玩家进退与死亡(join / leave / death):在时间窗口内收集缓冲,合并为单条消息发送,避免刷屏
|
|
6
|
+
* - 关键运行态事件(crash / start):先立即冲刷(flush)当前窗口内的待发送事件,随后立即单独推送
|
|
7
|
+
*/
|
|
8
|
+
import { type QqEventsConfig } from "@sfmc-bds/sdk/node/config";
|
|
9
|
+
import type { OutboundConfig } from "./bridge.js";
|
|
10
|
+
export type QqEventType = "join" | "leave" | "death" | "crash" | "start";
|
|
11
|
+
export type QqEventPayload = {
|
|
12
|
+
type: QqEventType;
|
|
13
|
+
player?: string;
|
|
14
|
+
cause?: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
};
|
|
17
|
+
export type ResolvedQqEventsConfig = Required<QqEventsConfig>;
|
|
18
|
+
/** 窗口内条数上限:达到上限时提前触发 flush,避免单次积压过多内容。 */
|
|
19
|
+
export declare const MAX_WINDOW_EVENTS = 20;
|
|
20
|
+
/**
|
|
21
|
+
* 解析并填充 QQ 事件推送配置的默认值。
|
|
22
|
+
*
|
|
23
|
+
* @param raw 原始配置对象。
|
|
24
|
+
* @returns 规范化的完整配置对象。
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveQqEventsConfig(raw: unknown): ResolvedQqEventsConfig;
|
|
27
|
+
/**
|
|
28
|
+
* 将 SAPI 原生伤害来源代码(damageSource.cause)转换为易读的中文文本。
|
|
29
|
+
*
|
|
30
|
+
* @param cause 伤害来源标识。
|
|
31
|
+
* @returns 中文伤害描述(未收录时保留原英文代码)。
|
|
32
|
+
*/
|
|
33
|
+
export declare function localizeCause(cause: string | undefined): string;
|
|
34
|
+
export declare function isEventTypeEnabled(cfg: ResolvedQqEventsConfig, type: QqEventType): boolean;
|
|
35
|
+
type Buffered = {
|
|
36
|
+
type: "join" | "leave" | "death";
|
|
37
|
+
player: string;
|
|
38
|
+
cause?: string;
|
|
39
|
+
};
|
|
40
|
+
export type QqEventsAggregatorDeps = {
|
|
41
|
+
getConfig: () => ResolvedQqEventsConfig;
|
|
42
|
+
getOutbound: () => OutboundConfig;
|
|
43
|
+
/** 可选注入:供单元测试模拟时钟与定时器。 */
|
|
44
|
+
setTimeoutFn?: (fn: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
45
|
+
clearTimeoutFn?: (id: ReturnType<typeof setTimeout>) => void;
|
|
46
|
+
send?: (text: string) => void;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* 格式化时间窗口内的聚合事件文本。
|
|
50
|
+
*
|
|
51
|
+
* @param events 缓冲区内的事件列表。
|
|
52
|
+
* @returns 格式化后的多行消息字符串。
|
|
53
|
+
*/
|
|
54
|
+
export declare function formatWindowBody(events: Buffered[]): string;
|
|
55
|
+
/**
|
|
56
|
+
* 格式化高优先级的非聚合即时事件文本(如 BDS 崩溃或启动就绪)。
|
|
57
|
+
*
|
|
58
|
+
* @param ev 即时事件载荷。
|
|
59
|
+
* @returns 格式化后的事件通知文本。
|
|
60
|
+
*/
|
|
61
|
+
export declare function formatImmediateBody(ev: QqEventPayload): string;
|
|
62
|
+
export declare function normalizeEventPayload(raw: unknown): QqEventPayload | null;
|
|
63
|
+
/**
|
|
64
|
+
* 创建 QQ 服务器事件聚合节流器实例。
|
|
65
|
+
*
|
|
66
|
+
* @param deps 依赖注入对象(包含配置获取、出站发送及定时器实现)。
|
|
67
|
+
* @returns 包含 push, flush, cancel, getPendingCount 等方法的聚合器控制器。
|
|
68
|
+
*/
|
|
69
|
+
export declare function createQqEventsAggregator(deps: QqEventsAggregatorDeps): {
|
|
70
|
+
ingestOne: (ev: QqEventPayload) => {
|
|
71
|
+
accepted: boolean;
|
|
72
|
+
reason?: string;
|
|
73
|
+
};
|
|
74
|
+
ingestMany: (items: QqEventPayload[]) => {
|
|
75
|
+
accepted: number;
|
|
76
|
+
rejected: number;
|
|
77
|
+
};
|
|
78
|
+
flushWindow: () => void;
|
|
79
|
+
dispose: () => void;
|
|
80
|
+
pendingCount: () => number;
|
|
81
|
+
};
|
|
82
|
+
export type QqEventsAggregator = ReturnType<typeof createQqEventsAggregator>;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=qq-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qq-events.d.ts","sourceRoot":"","sources":["../../src/domain/qq-events.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAGlD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAEzE,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC,cAAc,CAAC,CAAC;AAK9D,0CAA0C;AAC1C,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAuCpC;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,sBAAsB,CAe1E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAK/D;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,sBAAsB,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAG1F;AAED,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,sBAAsB,CAAC;IACxC,WAAW,EAAE,MAAM,cAAc,CAAC;IAClC,0BAA0B;IAC1B,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;IAC7E,cAAc,CAAC,EAAE,CAAC,EAAE,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,KAAK,IAAI,CAAC;IAC7D,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAiB3D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,cAAc,GAAG,MAAM,CAU9D;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAmBzE;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB;oBAqC5C,cAAc,KAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;wBAyBnD,cAAc,EAAE,KAAG;QAC5C,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB;uBA7CuB,IAAI;mBAyDR,IAAI;wBAKC,MAAM;EAKhC;AAED,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|