@sfmc-bds/db-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +21 -0
  2. package/dist/db-tables.d.ts +13 -0
  3. package/dist/db-tables.d.ts.map +1 -0
  4. package/dist/db-tables.js +35 -0
  5. package/dist/db-tables.js.map +1 -0
  6. package/dist/domain/bridge.d.ts +48 -0
  7. package/dist/domain/bridge.d.ts.map +1 -0
  8. package/dist/domain/bridge.js +82 -0
  9. package/dist/domain/bridge.js.map +1 -0
  10. package/dist/domain/economy.d.ts +120 -0
  11. package/dist/domain/economy.d.ts.map +1 -0
  12. package/dist/domain/economy.js +321 -0
  13. package/dist/domain/economy.js.map +1 -0
  14. package/dist/domain/schema.d.ts +26 -0
  15. package/dist/domain/schema.d.ts.map +1 -0
  16. package/dist/domain/schema.js +462 -0
  17. package/dist/domain/schema.js.map +1 -0
  18. package/dist/domain/transaction.d.ts +67 -0
  19. package/dist/domain/transaction.d.ts.map +1 -0
  20. package/dist/domain/transaction.js +85 -0
  21. package/dist/domain/transaction.js.map +1 -0
  22. package/dist/env.d.ts +22 -0
  23. package/dist/env.d.ts.map +1 -0
  24. package/dist/env.js +94 -0
  25. package/dist/env.js.map +1 -0
  26. package/dist/index.d.ts +33 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +313 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/lib/http.d.ts +18 -0
  31. package/dist/lib/http.d.ts.map +1 -0
  32. package/dist/lib/http.js +43 -0
  33. package/dist/lib/http.js.map +1 -0
  34. package/dist/lib/idempotency-store.d.ts +28 -0
  35. package/dist/lib/idempotency-store.d.ts.map +1 -0
  36. package/dist/lib/idempotency-store.js +49 -0
  37. package/dist/lib/idempotency-store.js.map +1 -0
  38. package/dist/lib/idempotency.d.ts +9 -0
  39. package/dist/lib/idempotency.d.ts.map +1 -0
  40. package/dist/lib/idempotency.js +14 -0
  41. package/dist/lib/idempotency.js.map +1 -0
  42. package/dist/lib/identifiers.d.ts +3 -0
  43. package/dist/lib/identifiers.d.ts.map +1 -0
  44. package/dist/lib/identifiers.js +18 -0
  45. package/dist/lib/identifiers.js.map +1 -0
  46. package/dist/lib/log.d.ts +11 -0
  47. package/dist/lib/log.d.ts.map +1 -0
  48. package/dist/lib/log.js +15 -0
  49. package/dist/lib/log.js.map +1 -0
  50. package/dist/lib/module-state.d.ts +15 -0
  51. package/dist/lib/module-state.d.ts.map +1 -0
  52. package/dist/lib/module-state.js +27 -0
  53. package/dist/lib/module-state.js.map +1 -0
  54. package/dist/lib/runtime.d.ts +21 -0
  55. package/dist/lib/runtime.d.ts.map +1 -0
  56. package/dist/lib/runtime.js +34 -0
  57. package/dist/lib/runtime.js.map +1 -0
  58. package/dist/lib/sql-helpers.d.ts +31 -0
  59. package/dist/lib/sql-helpers.d.ts.map +1 -0
  60. package/dist/lib/sql-helpers.js +30 -0
  61. package/dist/lib/sql-helpers.js.map +1 -0
  62. package/dist/lib/sqlite.d.ts +20 -0
  63. package/dist/lib/sqlite.d.ts.map +1 -0
  64. package/dist/lib/sqlite.js +59 -0
  65. package/dist/lib/sqlite.js.map +1 -0
  66. package/dist/manifest-loader.d.ts +69 -0
  67. package/dist/manifest-loader.d.ts.map +1 -0
  68. package/dist/manifest-loader.js +212 -0
  69. package/dist/manifest-loader.js.map +1 -0
  70. package/dist/manifest.d.ts +76 -0
  71. package/dist/manifest.d.ts.map +1 -0
  72. package/dist/manifest.js +128 -0
  73. package/dist/manifest.js.map +1 -0
  74. package/dist/module-auth.d.ts +58 -0
  75. package/dist/module-auth.d.ts.map +1 -0
  76. package/dist/module-auth.js +118 -0
  77. package/dist/module-auth.js.map +1 -0
  78. package/dist/permission-gate.d.ts +53 -0
  79. package/dist/permission-gate.d.ts.map +1 -0
  80. package/dist/permission-gate.js +94 -0
  81. package/dist/permission-gate.js.map +1 -0
  82. package/dist/routes/_shared.d.ts +68 -0
  83. package/dist/routes/_shared.d.ts.map +1 -0
  84. package/dist/routes/_shared.js +27 -0
  85. package/dist/routes/_shared.js.map +1 -0
  86. package/dist/routes/config.d.ts +27 -0
  87. package/dist/routes/config.d.ts.map +1 -0
  88. package/dist/routes/config.js +236 -0
  89. package/dist/routes/config.js.map +1 -0
  90. package/dist/routes/db-routes.d.ts +38 -0
  91. package/dist/routes/db-routes.d.ts.map +1 -0
  92. package/dist/routes/db-routes.js +186 -0
  93. package/dist/routes/db-routes.js.map +1 -0
  94. package/dist/routes/health.d.ts +10 -0
  95. package/dist/routes/health.d.ts.map +1 -0
  96. package/dist/routes/health.js +23 -0
  97. package/dist/routes/health.js.map +1 -0
  98. package/dist/routes/messages.d.ts +23 -0
  99. package/dist/routes/messages.d.ts.map +1 -0
  100. package/dist/routes/messages.js +72 -0
  101. package/dist/routes/messages.js.map +1 -0
  102. package/dist/routes/module-config-routes.d.ts +31 -0
  103. package/dist/routes/module-config-routes.d.ts.map +1 -0
  104. package/dist/routes/module-config-routes.js +128 -0
  105. package/dist/routes/module-config-routes.js.map +1 -0
  106. package/dist/routes/modules.d.ts +34 -0
  107. package/dist/routes/modules.d.ts.map +1 -0
  108. package/dist/routes/modules.js +92 -0
  109. package/dist/routes/modules.js.map +1 -0
  110. package/dist/routes/service-routes.d.ts +26 -0
  111. package/dist/routes/service-routes.d.ts.map +1 -0
  112. package/dist/routes/service-routes.js +68 -0
  113. package/dist/routes/service-routes.js.map +1 -0
  114. package/dist/runtime.test.d.ts +2 -0
  115. package/dist/runtime.test.d.ts.map +1 -0
  116. package/dist/runtime.test.js +66 -0
  117. package/dist/runtime.test.js.map +1 -0
  118. package/dist/schema-registry.d.ts +63 -0
  119. package/dist/schema-registry.d.ts.map +1 -0
  120. package/dist/schema-registry.js +178 -0
  121. package/dist/schema-registry.js.map +1 -0
  122. package/dist/server.d.ts +27 -0
  123. package/dist/server.d.ts.map +1 -0
  124. package/dist/server.js +82 -0
  125. package/dist/server.js.map +1 -0
  126. package/dist/service-registry.d.ts +46 -0
  127. package/dist/service-registry.d.ts.map +1 -0
  128. package/dist/service-registry.js +73 -0
  129. package/dist/service-registry.js.map +1 -0
  130. package/dist/services/builtin-handlers.d.ts +22 -0
  131. package/dist/services/builtin-handlers.d.ts.map +1 -0
  132. package/dist/services/builtin-handlers.js +23 -0
  133. package/dist/services/builtin-handlers.js.map +1 -0
  134. package/dist/services/economy-handlers.d.ts +14 -0
  135. package/dist/services/economy-handlers.d.ts.map +1 -0
  136. package/dist/services/economy-handlers.js +144 -0
  137. package/dist/services/economy-handlers.js.map +1 -0
  138. package/dist/tx-runner.d.ts +144 -0
  139. package/dist/tx-runner.d.ts.map +1 -0
  140. package/dist/tx-runner.js +273 -0
  141. package/dist/tx-runner.js.map +1 -0
  142. package/dist/where.d.ts +48 -0
  143. package/dist/where.d.ts.map +1 -0
  144. package/dist/where.js +107 -0
  145. package/dist/where.js.map +1 -0
  146. package/package.json +52 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @sfmc-bds/db-server
2
+
3
+ SFMC SQLite HTTP REST 后端:配置快照、模块启停、游戏数据路由、QQ 消息中继。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @sfmc-bds/db-server
9
+ node node_modules/@sfmc-bds/db-server/dist/index.js
10
+ ```
11
+
12
+ 通常由 `@sfmc-bds/cli` 或 SEA 包统一拉起,无需单独安装。
13
+
14
+ ## 依赖
15
+
16
+ - `@sfmc-bds/sdk` ^0.1.0
17
+ - Node.js >= 22.13(`node:sqlite`)
18
+
19
+ ## 仓库
20
+
21
+ <https://github.com/DogeLakeDev/ScriptsForMinecraftServer/tree/main/db-server>
@@ -0,0 +1,13 @@
1
+ /**
2
+ * db-tables.ts — 平台自带数据库表 bootstrap
3
+ *
4
+ * 平台表(不是模块声明的):
5
+ * - sfmc__audit — db.audit() 自动写入
6
+ * - sfmc__idempotent — db.idempotent() probe/commit 用
7
+ *
8
+ * 业务表(land / economy / coops / redpacket / ...)的 DDL 不在这里;
9
+ * 它们由各自模块在 init() 调 db.defineTable() 声明,schema-registry 统一建表。
10
+ */
11
+ import type { DatabaseSync } from "node:sqlite";
12
+ export declare function createPlatformTables(db: DatabaseSync): void;
13
+ //# sourceMappingURL=db-tables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-tables.d.ts","sourceRoot":"","sources":["../src/db-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAwB3D"}
@@ -0,0 +1,35 @@
1
+ /**
2
+ * db-tables.ts — 平台自带数据库表 bootstrap
3
+ *
4
+ * 平台表(不是模块声明的):
5
+ * - sfmc__audit — db.audit() 自动写入
6
+ * - sfmc__idempotent — db.idempotent() probe/commit 用
7
+ *
8
+ * 业务表(land / economy / coops / redpacket / ...)的 DDL 不在这里;
9
+ * 它们由各自模块在 init() 调 db.defineTable() 声明,schema-registry 统一建表。
10
+ */
11
+ export function createPlatformTables(db) {
12
+ db.exec(/* sql */ `
13
+ CREATE TABLE IF NOT EXISTS sfmc__audit (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ module_id TEXT NOT NULL,
16
+ table_name TEXT NOT NULL,
17
+ row_id TEXT NOT NULL,
18
+ action TEXT NOT NULL,
19
+ data TEXT,
20
+ created_at TEXT NOT NULL
21
+ )
22
+ `);
23
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_sfmc__audit_module_table ON sfmc__audit(module_id, table_name)`);
24
+ db.exec(/* sql */ `
25
+ CREATE TABLE IF NOT EXISTS sfmc__idempotent (
26
+ module_id TEXT NOT NULL,
27
+ action TEXT NOT NULL,
28
+ key TEXT NOT NULL,
29
+ value TEXT,
30
+ created_at TEXT NOT NULL,
31
+ PRIMARY KEY (module_id, action, key)
32
+ )
33
+ `);
34
+ }
35
+ //# sourceMappingURL=db-tables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-tables.js","sourceRoot":"","sources":["../src/db-tables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,UAAU,oBAAoB,CAAC,EAAgB;IACnD,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;;;;;;;;;;GAUjB,CAAC,CAAC;IACH,EAAE,CAAC,IAAI,CAAC,+FAA+F,CAAC,CAAC;IAEzG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC;;;;;;;;;GASjB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * domain/bridge.ts — MC → QQ 桥接
3
+ *
4
+ * db-server 收到 SAPI 上报的消息后,直接调 LLBot HTTP `/send_group_msg`
5
+ * 把消息转发到 QQ 群。不再走中间 qq-bridge 进程。
6
+ *
7
+ * 数据流:
8
+ * SAPI ──POST──→ db-server:3001/api/sfmc/messages
9
+ * └─ 写库 + forwardToQQBridge() ──HTTP──→ LLBot:3004/send_group_msg
10
+ *
11
+ * 事务描述:
12
+ * - 无 DB 事务(本文件不操作 SQLite,只做 HTTP 出站转发)
13
+ *
14
+ * 领域函数:
15
+ * - makeLLBotConfig(env) 工厂:从 env 构造 LLBotConfig
16
+ * - forwardToQQBridge() 转发单条 MC 消息到 LLBot;失败仅 warn,不抛错
17
+ */
18
+ type LLBotConfig = {
19
+ host: string;
20
+ port: number;
21
+ token: string;
22
+ groupId: string;
23
+ prefix: string;
24
+ };
25
+ /**
26
+ * 构造 LLBotConfig。允许显式传入(测试),默认从 env 推不出,
27
+ * 所以调用方需自己把 env 里读到的值传进来。
28
+ */
29
+ export declare function makeLLBotConfig(env: {
30
+ LLBOT_HOST: string;
31
+ LLBOT_PORT: number;
32
+ LLBOT_TOKEN: string;
33
+ QQ_GROUP_ID: string;
34
+ }): LLBotConfig;
35
+ /**
36
+ * @description
37
+ * @author Shiroha7z
38
+ * @date 17/07/2026
39
+ * @export
40
+ * @param {LLBotConfig} config LLBot 连接信息(host/port/token/groupId/prefix)
41
+ * @param {string} channelId MC 频道 ID(用于过滤 / 标记,目前仅做日志)
42
+ * @param {string} fromName MC 玩家名
43
+ * @param {string} content 消息正文
44
+ * @param {string} fromId MC 玩家 ID
45
+ */
46
+ export declare function forwardToQQBridge(config: LLBotConfig, channelId: string, fromName: string, content: string, fromId: string): void;
47
+ export {};
48
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/domain/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,WAAW,CAQd;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,IAAI,CA6CN"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * domain/bridge.ts — MC → QQ 桥接
3
+ *
4
+ * db-server 收到 SAPI 上报的消息后,直接调 LLBot HTTP `/send_group_msg`
5
+ * 把消息转发到 QQ 群。不再走中间 qq-bridge 进程。
6
+ *
7
+ * 数据流:
8
+ * SAPI ──POST──→ db-server:3001/api/sfmc/messages
9
+ * └─ 写库 + forwardToQQBridge() ──HTTP──→ LLBot:3004/send_group_msg
10
+ *
11
+ * 事务描述:
12
+ * - 无 DB 事务(本文件不操作 SQLite,只做 HTTP 出站转发)
13
+ *
14
+ * 领域函数:
15
+ * - makeLLBotConfig(env) 工厂:从 env 构造 LLBotConfig
16
+ * - forwardToQQBridge() 转发单条 MC 消息到 LLBot;失败仅 warn,不抛错
17
+ */
18
+ import { request } from "node:http";
19
+ import { log } from "../lib/log.js";
20
+ /**
21
+ * 构造 LLBotConfig。允许显式传入(测试),默认从 env 推不出,
22
+ * 所以调用方需自己把 env 里读到的值传进来。
23
+ */
24
+ export function makeLLBotConfig(env) {
25
+ return {
26
+ host: env.LLBOT_HOST,
27
+ port: env.LLBOT_PORT,
28
+ token: env.LLBOT_TOKEN,
29
+ groupId: env.QQ_GROUP_ID,
30
+ prefix: "[MC]",
31
+ };
32
+ }
33
+ /**
34
+ * @description
35
+ * @author Shiroha7z
36
+ * @date 17/07/2026
37
+ * @export
38
+ * @param {LLBotConfig} config LLBot 连接信息(host/port/token/groupId/prefix)
39
+ * @param {string} channelId MC 频道 ID(用于过滤 / 标记,目前仅做日志)
40
+ * @param {string} fromName MC 玩家名
41
+ * @param {string} content 消息正文
42
+ * @param {string} fromId MC 玩家 ID
43
+ */
44
+ export function forwardToQQBridge(config, channelId, fromName, content, fromId) {
45
+ if (!config.groupId || config.groupId === "0") {
46
+ log.warn(`QQ 群未配置 (qq_group_id),跳过 MC→QQ 转发 (channel=${channelId})`);
47
+ return;
48
+ }
49
+ const text = `${config.prefix} ${fromName}: ${content}`;
50
+ const payload = JSON.stringify({
51
+ group_id: parseInt(config.groupId, 10),
52
+ message: [{ type: "text", data: { text } }],
53
+ });
54
+ const headers = {
55
+ "Content-Type": "application/json",
56
+ "Content-Length": Buffer.byteLength(payload),
57
+ };
58
+ if (config.token) {
59
+ headers["Authorization"] = `Bearer ${config.token}`;
60
+ }
61
+ const req = request({
62
+ hostname: config.host,
63
+ port: config.port,
64
+ path: "/send_group_msg",
65
+ method: "POST",
66
+ headers,
67
+ }, (res) => {
68
+ let body = "";
69
+ res.on("data", (c) => (body += c));
70
+ res.on("end", () => {
71
+ if (res.statusCode !== 200) {
72
+ log.warn(`LLBot send_group_msg → ${res.statusCode}: ${String(body).slice(0, 200)} (from=${fromId}, channel=${channelId})`);
73
+ }
74
+ });
75
+ });
76
+ req.on("error", (err) => {
77
+ log.warn(`LLBot 不可达 (${config.host}:${config.port}): ${err.message}`);
78
+ });
79
+ req.write(payload);
80
+ req.end();
81
+ }
82
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../../src/domain/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAUpC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,GAK/B;IACC,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,UAAU;QACpB,IAAI,EAAE,GAAG,CAAC,UAAU;QACpB,KAAK,EAAE,GAAG,CAAC,WAAW;QACtB,OAAO,EAAE,GAAG,CAAC,WAAW;QACxB,MAAM,EAAE,MAAM;KACf,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAmB,EACnB,SAAiB,EACjB,QAAgB,EAChB,OAAe,EACf,MAAc;IAEd,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,8CAA8C,SAAS,GAAG,CAAC,CAAC;QACrE,OAAO;IACT,CAAC;IAED,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;IACxD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;QAC7B,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QACtC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC;KAC5C,CAAC,CAAC;IAEH,MAAM,OAAO,GAAoC;QAC/C,cAAc,EAAE,kBAAkB;QAClC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC;KAC7C,CAAC;IACF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,OAAO,CAAC,eAAe,CAAC,GAAG,UAAU,MAAM,CAAC,KAAK,EAAE,CAAC;IACtD,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,CACjB;QACE,QAAQ,EAAE,MAAM,CAAC,IAAI;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,iBAAiB;QACvB,MAAM,EAAE,MAAM;QACd,OAAO;KACR,EACD,CAAC,GAAG,EAAE,EAAE;QACN,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QACnC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACjB,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,CACN,0BAA0B,GAAG,CAAC,UAAU,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,UAAU,MAAM,aAAa,SAAS,GAAG,CACjH,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CACF,CAAC;IACF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;QACtB,GAAG,CAAC,IAAI,CAAC,cAAc,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnB,GAAG,CAAC,GAAG,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * domain/economy.ts — 经济系统业务核心
3
+ *
4
+ * 提供账户 upsert、通用转账、日常任务奖励与月度统计。
5
+ * land / chat / gui 等通过 service(economy.account.*) 调用本文件。
6
+ *
7
+ * 事务约定(方案 A):
8
+ * - applyEconomySteps / submitDailyTaskSteps — 无 BEGIN/COMMIT,可嵌进外层 db.tx
9
+ * - applyEconomyTransaction / submitDailyTaskTx — 独立调用时自己开事务;
10
+ * alreadyInTx=true 时只跑 steps
11
+ */
12
+ import type { EconomyAccountRow, EconomyTransactionRow } from "@sfmc-bds/sdk/contracts";
13
+ import type { DatabaseSync } from "node:sqlite";
14
+ import type { SQLStatement } from "sql-template-strings";
15
+ import type { TxResult } from "./transaction.js";
16
+ export type { TxResult };
17
+ /** query 兼容格式:string / SQLStatement / { sql, values } */
18
+ export type AnyQuery = (sql: string | SQLStatement | {
19
+ sql: string;
20
+ values?: unknown[];
21
+ }, values?: unknown[]) => unknown[] | {
22
+ changes: number | bigint;
23
+ };
24
+ export interface EconomyAccountView {
25
+ playerId: string;
26
+ playerName: string;
27
+ balance: number;
28
+ version: number;
29
+ }
30
+ export declare function economyResult(row: EconomyAccountRow | undefined): EconomyAccountView | undefined;
31
+ /** 确保账户存在并返回最新行 */
32
+ export declare function ensureEconomyAccount(query: AnyQuery, playerId: string, playerName: string): EconomyAccountRow | undefined;
33
+ /** 读取(必要时创建)账户业务视图 */
34
+ export declare function getEconomyAccount(query: AnyQuery, playerId: string, playerName?: string): EconomyAccountView | null;
35
+ export interface ApplyEconomyInput {
36
+ actor_id?: string;
37
+ amount: number;
38
+ transaction_type?: string;
39
+ source_player_id?: string;
40
+ target_player_id?: string;
41
+ reference_type?: string;
42
+ reference_id?: string;
43
+ reason?: string;
44
+ idempotency_key?: string;
45
+ actorId?: string;
46
+ type?: string;
47
+ sourcePlayerId?: string;
48
+ targetPlayerId?: string;
49
+ sourcePlayerName?: string;
50
+ targetPlayerName?: string;
51
+ referenceType?: string;
52
+ referenceId?: string;
53
+ reasonAlias?: string;
54
+ idempotencyKey?: string;
55
+ }
56
+ export type ApplyEconomyResult = {
57
+ ok: true;
58
+ transactionId: string;
59
+ replayed?: boolean;
60
+ source: (EconomyAccountView & {
61
+ balanceBefore: number;
62
+ balanceAfter: number;
63
+ }) | null;
64
+ target: (EconomyAccountView & {
65
+ balanceBefore: number;
66
+ balanceAfter: number;
67
+ }) | null;
68
+ } | {
69
+ ok: false;
70
+ error: string;
71
+ status: number;
72
+ balance?: number;
73
+ };
74
+ /**
75
+ * 经济变更核心(无 BEGIN/COMMIT)。
76
+ * 纯 debit(仅 source)或纯 credit(仅 target)均可成功。
77
+ */
78
+ export declare function applyEconomySteps(query: AnyQuery, data: ApplyEconomyInput): ApplyEconomyResult;
79
+ /**
80
+ * 独立经济事务包装。alreadyInTx=true 时复用外层事务(不再 BEGIN)。
81
+ */
82
+ export declare function applyEconomyTransaction(query: AnyQuery, db: DatabaseSync, data: ApplyEconomyInput, opts?: {
83
+ alreadyInTx?: boolean;
84
+ }): ApplyEconomyResult;
85
+ export type { EconomyAccountRow, EconomyTransactionRow };
86
+ /** 列出日常任务(默认仅 active 且未过期) */
87
+ export declare function listDailyTasks(query: AnyQuery, filter?: {
88
+ status?: string;
89
+ includeExpired?: boolean;
90
+ }): Array<Record<string, unknown>>;
91
+ export interface SubmitDailyTaskInput {
92
+ actorId: string;
93
+ taskId: string;
94
+ quantity: number;
95
+ }
96
+ /** 日常任务提交核心(无 BEGIN) */
97
+ export declare function submitDailyTaskSteps(query: AnyQuery, data: SubmitDailyTaskInput): TxResult<{
98
+ transactionId: string;
99
+ reward: number;
100
+ balance: number;
101
+ balanceVersion: number;
102
+ }>;
103
+ /** 日常任务提交包装(可独立事务或嵌入外层 tx) */
104
+ export declare function submitDailyTaskTx(query: AnyQuery, db: DatabaseSync, data: SubmitDailyTaskInput, opts?: {
105
+ alreadyInTx?: boolean;
106
+ }): TxResult<{
107
+ transactionId: string;
108
+ reward: number;
109
+ balance: number;
110
+ balanceVersion: number;
111
+ }>;
112
+ /** 月度/全局经济白皮书(优先读 sfmc_economy_stats,否则现场聚合) */
113
+ export declare function monthlyEconomyStats(query: AnyQuery): {
114
+ id: string;
115
+ total_issued: number;
116
+ total_destroyed: number;
117
+ total_supply: number;
118
+ active_accounts: number;
119
+ };
120
+ //# sourceMappingURL=economy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"economy.d.ts","sourceRoot":"","sources":["../../src/domain/economy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,321 @@
1
+ /**
2
+ * domain/economy.ts — 经济系统业务核心
3
+ *
4
+ * 提供账户 upsert、通用转账、日常任务奖励与月度统计。
5
+ * land / chat / gui 等通过 service(economy.account.*) 调用本文件。
6
+ *
7
+ * 事务约定(方案 A):
8
+ * - applyEconomySteps / submitDailyTaskSteps — 无 BEGIN/COMMIT,可嵌进外层 db.tx
9
+ * - applyEconomyTransaction / submitDailyTaskTx — 独立调用时自己开事务;
10
+ * alreadyInTx=true 时只跑 steps
11
+ */
12
+ import { isValidIdempotencyKey } from "../lib/idempotency.js";
13
+ import { sql } from "../lib/sql-helpers.js";
14
+ const TABLE_ACCOUNTS = "sfmc_economy_accounts";
15
+ const TABLE_TRANSACTIONS = "sfmc_economy_transactions";
16
+ const TABLE_IDEMPOTENCY = "sfmc_economy_idempotency";
17
+ const TABLE_DAILY = "sfmc_economy_daily_tasks";
18
+ const TABLE_STATS = "sfmc_economy_stats";
19
+ function newTransactionId(now) {
20
+ return `E${now.toString(36)}${Math.random().toString(36).slice(2, 8)}`;
21
+ }
22
+ export function economyResult(row) {
23
+ if (!row)
24
+ return undefined;
25
+ return {
26
+ playerId: row.player_id,
27
+ playerName: row.player_name_snapshot,
28
+ balance: row.balance,
29
+ version: row.version,
30
+ };
31
+ }
32
+ /** 确保账户存在并返回最新行 */
33
+ export function ensureEconomyAccount(query, playerId, playerName) {
34
+ const now = Date.now();
35
+ query(sql(`INSERT INTO ${TABLE_ACCOUNTS} (player_id, player_name_snapshot, balance, version, created_at, updated_at)
36
+ VALUES (?, ?, 0, 1, ?, ?)
37
+ ON CONFLICT(player_id) DO UPDATE SET
38
+ player_name_snapshot = excluded.player_name_snapshot,
39
+ updated_at = excluded.updated_at`, [String(playerId), String(playerName), now, now]));
40
+ const rows = query(sql(`SELECT * FROM ${TABLE_ACCOUNTS} WHERE player_id = ?`, [String(playerId)]));
41
+ if (Array.isArray(rows)) {
42
+ return rows[0];
43
+ }
44
+ return undefined;
45
+ }
46
+ /** 读取(必要时创建)账户业务视图 */
47
+ export function getEconomyAccount(query, playerId, playerName = "") {
48
+ const row = ensureEconomyAccount(query, playerId, playerName);
49
+ return economyResult(row) ?? null;
50
+ }
51
+ /**
52
+ * 经济变更核心(无 BEGIN/COMMIT)。
53
+ * 纯 debit(仅 source)或纯 credit(仅 target)均可成功。
54
+ */
55
+ export function applyEconomySteps(query, data) {
56
+ const amount = Number(data.amount);
57
+ const actorId = String(data.actor_id ?? data.actorId ?? "").trim();
58
+ if (!actorId || !Number.isSafeInteger(amount) || amount <= 0) {
59
+ return { ok: false, error: "invalid_amount", status: 400 };
60
+ }
61
+ const sourceId = data.source_player_id
62
+ ? String(data.source_player_id)
63
+ : data.sourcePlayerId
64
+ ? String(data.sourcePlayerId)
65
+ : null;
66
+ const targetId = data.target_player_id
67
+ ? String(data.target_player_id)
68
+ : data.targetPlayerId
69
+ ? String(data.targetPlayerId)
70
+ : null;
71
+ const idempotencyKey = String(data.idempotency_key ?? data.idempotencyKey ?? "").trim();
72
+ if (idempotencyKey && !isValidIdempotencyKey(idempotencyKey)) {
73
+ return { ok: false, error: "invalid_idempotency_key", status: 400 };
74
+ }
75
+ if (!sourceId && !targetId) {
76
+ return { ok: false, error: "missing_account", status: 400 };
77
+ }
78
+ if (sourceId && sourceId !== actorId) {
79
+ return { ok: false, error: "forbidden_source", status: 403 };
80
+ }
81
+ if (sourceId && targetId && sourceId === targetId) {
82
+ return { ok: false, error: "same_account", status: 400 };
83
+ }
84
+ // 幂等回放
85
+ if (idempotencyKey) {
86
+ const rows = query(sql(`SELECT response_json FROM ${TABLE_IDEMPOTENCY}
87
+ WHERE actor_id = ? AND idempotency_key = ?`, [actorId, idempotencyKey]));
88
+ const previous = Array.isArray(rows) ? rows[0] : undefined;
89
+ if (previous) {
90
+ return { ...JSON.parse(previous.response_json), replayed: true };
91
+ }
92
+ }
93
+ const source = sourceId ? ensureEconomyAccount(query, sourceId, String(data.sourcePlayerName ?? "")) : null;
94
+ const target = targetId ? ensureEconomyAccount(query, targetId, String(data.targetPlayerName ?? "")) : null;
95
+ if (sourceId && !source) {
96
+ return { ok: false, error: "source_not_found", status: 404 };
97
+ }
98
+ if (targetId && !target) {
99
+ return { ok: false, error: "target_not_found", status: 404 };
100
+ }
101
+ if (source && source.balance < amount) {
102
+ return { ok: false, error: "insufficient_funds", balance: source.balance, status: 409 };
103
+ }
104
+ const now = Date.now();
105
+ if (source) {
106
+ query(sql(`UPDATE ${TABLE_ACCOUNTS}
107
+ SET balance = balance - ?, version = version + 1, updated_at = ?
108
+ WHERE player_id = ? AND balance >= ?`, [amount, now, source.player_id, amount]));
109
+ }
110
+ if (target) {
111
+ query(sql(`UPDATE ${TABLE_ACCOUNTS}
112
+ SET balance = balance + ?, version = version + 1, updated_at = ?
113
+ WHERE player_id = ?`, [amount, now, target.player_id]));
114
+ }
115
+ const transactionType = String(data.transaction_type ?? data.type ?? "adjustment");
116
+ const referenceType = String(data.reference_type ?? data.referenceType ?? "");
117
+ const referenceId = String(data.reference_id ?? data.referenceId ?? "");
118
+ const reason = String(data.reason ?? data.reasonAlias ?? "");
119
+ const id = newTransactionId(now);
120
+ if (source) {
121
+ query(sql(`INSERT INTO ${TABLE_TRANSACTIONS}
122
+ (id, transaction_type, actor_id, source_player_id, target_player_id,
123
+ amount, balance_before, balance_after,
124
+ reference_type, reference_id, reason, created_at)
125
+ VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)`, [
126
+ id + "-dr",
127
+ transactionType + ".dr",
128
+ actorId,
129
+ sourceId,
130
+ amount,
131
+ source.balance,
132
+ source.balance - amount,
133
+ referenceType,
134
+ referenceId,
135
+ reason,
136
+ now,
137
+ ]));
138
+ }
139
+ if (target) {
140
+ query(sql(`INSERT INTO ${TABLE_TRANSACTIONS}
141
+ (id, transaction_type, actor_id, source_player_id, target_player_id,
142
+ amount, balance_before, balance_after,
143
+ reference_type, reference_id, reason, created_at)
144
+ VALUES (?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`, [
145
+ id + "-cr",
146
+ transactionType + ".cr",
147
+ actorId,
148
+ targetId,
149
+ amount,
150
+ target.balance,
151
+ target.balance + amount,
152
+ referenceType,
153
+ referenceId,
154
+ reason,
155
+ now,
156
+ ]));
157
+ }
158
+ const sourceView = source ? economyResult(ensureEconomyAccount(query, source.player_id, "")) : undefined;
159
+ const targetView = target ? economyResult(ensureEconomyAccount(query, target.player_id, "")) : undefined;
160
+ const response = {
161
+ ok: true,
162
+ transactionId: id,
163
+ source: sourceView
164
+ ? { ...sourceView, balanceBefore: source.balance, balanceAfter: source.balance - amount }
165
+ : null,
166
+ target: targetView
167
+ ? { ...targetView, balanceBefore: target.balance, balanceAfter: target.balance + amount }
168
+ : null,
169
+ };
170
+ if (idempotencyKey) {
171
+ query(sql(`INSERT INTO ${TABLE_IDEMPOTENCY}
172
+ (actor_id, idempotency_key, transaction_id, response_json, created_at)
173
+ VALUES (?, ?, ?, ?, ?)`, [actorId, idempotencyKey, id, JSON.stringify(response), now]));
174
+ }
175
+ return response;
176
+ }
177
+ /**
178
+ * 独立经济事务包装。alreadyInTx=true 时复用外层事务(不再 BEGIN)。
179
+ */
180
+ export function applyEconomyTransaction(query, db, data, opts) {
181
+ if (opts?.alreadyInTx) {
182
+ return applyEconomySteps(query, data);
183
+ }
184
+ db.exec("BEGIN IMMEDIATE");
185
+ try {
186
+ const result = applyEconomySteps(query, data);
187
+ if (!result.ok) {
188
+ db.exec("ROLLBACK");
189
+ return result;
190
+ }
191
+ db.exec("COMMIT");
192
+ return result;
193
+ }
194
+ catch (error) {
195
+ try {
196
+ db.exec("ROLLBACK");
197
+ }
198
+ catch {
199
+ /* ignore */
200
+ }
201
+ return {
202
+ ok: false,
203
+ error: error.message || "economy_transaction_failed",
204
+ status: 500,
205
+ };
206
+ }
207
+ }
208
+ /** 列出日常任务(默认仅 active 且未过期) */
209
+ export function listDailyTasks(query, filter) {
210
+ const status = filter?.status ?? "active";
211
+ const now = Date.now();
212
+ if (filter?.includeExpired) {
213
+ const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ?`, [status]));
214
+ return Array.isArray(rows) ? rows : [];
215
+ }
216
+ const rows = query(sql(`SELECT * FROM ${TABLE_DAILY} WHERE status = ? AND expires_at > ?`, [status, now]));
217
+ return Array.isArray(rows) ? rows : [];
218
+ }
219
+ /** 日常任务提交核心(无 BEGIN) */
220
+ export function submitDailyTaskSteps(query, data) {
221
+ const actorId = data.actorId;
222
+ const taskId = data.taskId;
223
+ const quantity = data.quantity;
224
+ const rows = query(sql(`SELECT * FROM ${TABLE_DAILY}
225
+ WHERE id = ? AND status = 'active' AND expires_at > ?`, [taskId, Date.now()]));
226
+ const task = rows[0];
227
+ if (!task) {
228
+ return { ok: false, error: "task_not_found_or_expired", status: 404 };
229
+ }
230
+ const targetQty = Number(task.target_qty ?? 0);
231
+ const filledQty = Number(task.filled_qty ?? 0);
232
+ const remaining = targetQty - filledQty;
233
+ if (remaining < quantity) {
234
+ return { ok: false, error: "task_quota_exceeded", status: 409, extra: { remaining } };
235
+ }
236
+ const reward = Number(task.unit_reward) * quantity;
237
+ query(sql(`UPDATE ${TABLE_DAILY} SET filled_qty = filled_qty + ? WHERE id = ?`, [quantity, taskId]));
238
+ const result = applyEconomySteps(query, {
239
+ actor_id: actorId,
240
+ transaction_type: "daily_task.reward",
241
+ target_player_id: actorId,
242
+ amount: reward,
243
+ reference_type: "daily_task",
244
+ reference_id: taskId,
245
+ reason: `提交任务 ${task.item_type}*${quantity}`,
246
+ });
247
+ if (!result.ok) {
248
+ return { ok: false, error: result.error, status: result.status };
249
+ }
250
+ const tgtFinal = economyResult(ensureEconomyAccount(query, actorId, ""));
251
+ return {
252
+ ok: true,
253
+ data: {
254
+ transactionId: result.transactionId,
255
+ reward,
256
+ balance: tgtFinal?.balance ?? 0,
257
+ balanceVersion: tgtFinal?.version ?? 0,
258
+ },
259
+ };
260
+ }
261
+ /** 日常任务提交包装(可独立事务或嵌入外层 tx) */
262
+ export function submitDailyTaskTx(query, db, data, opts) {
263
+ if (opts?.alreadyInTx) {
264
+ return submitDailyTaskSteps(query, data);
265
+ }
266
+ db.exec("BEGIN IMMEDIATE");
267
+ try {
268
+ const result = submitDailyTaskSteps(query, data);
269
+ if (!result.ok) {
270
+ db.exec("ROLLBACK");
271
+ return result;
272
+ }
273
+ db.exec("COMMIT");
274
+ return result;
275
+ }
276
+ catch (error) {
277
+ try {
278
+ db.exec("ROLLBACK");
279
+ }
280
+ catch {
281
+ /* ignore */
282
+ }
283
+ return {
284
+ ok: false,
285
+ error: error.message || "task_submit_failed",
286
+ status: 500,
287
+ };
288
+ }
289
+ }
290
+ /** 月度/全局经济白皮书(优先读 sfmc_economy_stats,否则现场聚合) */
291
+ export function monthlyEconomyStats(query) {
292
+ const now = new Date();
293
+ const id = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
294
+ const cached = query(sql(`SELECT * FROM ${TABLE_STATS} WHERE id = ? OR id = ?`, ["global", id]));
295
+ if (Array.isArray(cached) && cached.length > 0) {
296
+ const row = cached[0];
297
+ return {
298
+ id: String(row.id ?? id),
299
+ total_issued: Number(row.total_issued ?? 0),
300
+ total_destroyed: Number(row.total_destroyed ?? 0),
301
+ total_supply: Number(row.total_supply ?? 0),
302
+ active_accounts: Number(row.active_accounts ?? 0),
303
+ };
304
+ }
305
+ const supplyRows = query(sql(`SELECT COALESCE(SUM(balance),0) AS total_supply, COUNT(*) AS active_accounts FROM ${TABLE_ACCOUNTS}`));
306
+ const supply = Array.isArray(supplyRows) ? supplyRows[0] : {};
307
+ const issuedRows = query(sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE target_player_id IS NOT NULL`));
308
+ const destroyedRows = query(sql(`SELECT COALESCE(SUM(amount),0) AS total FROM ${TABLE_TRANSACTIONS} WHERE source_player_id IS NOT NULL`));
309
+ const issued = Array.isArray(issuedRows) ? Number(issuedRows[0].total ?? 0) : 0;
310
+ const destroyed = Array.isArray(destroyedRows)
311
+ ? Number(destroyedRows[0].total ?? 0)
312
+ : 0;
313
+ return {
314
+ id,
315
+ total_issued: issued,
316
+ total_destroyed: destroyed,
317
+ total_supply: Number(supply.total_supply ?? 0),
318
+ active_accounts: Number(supply.active_accounts ?? 0),
319
+ };
320
+ }
321
+ //# sourceMappingURL=economy.js.map