From 29c921f49884eca514a98d54115dc80725b4146e Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 3 Mar 2026 14:49:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9B=B4=E6=94=B9=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 384 +--------------------------------------- package.json | 2 +- server/README.md | 170 ++++++++++++++++++ server/data.db | Bin 667648 -> 667648 bytes server/data.db-shm | Bin 32768 -> 32768 bytes server/data.db-wal | Bin 4132392 -> 4132392 bytes server/db.js | 224 ++++++++++++++--------- server/index.js | 12 +- server/seed.js | 5 +- server/situationData.js | 2 + 10 files changed, 338 insertions(+), 461 deletions(-) create mode 100644 server/README.md diff --git a/package-lock.json b/package-lock.json index 6fdedbf..ba297ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "us-iran-military-dashboard", "version": "1.0.0", "dependencies": { - "better-sqlite3": "^11.6.0", "cors": "^2.8.5", "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", @@ -19,6 +18,7 @@ "react-dom": "^18.3.1", "react-map-gl": "^7.1.7", "react-router-dom": "^7.13.1", + "sql.js": "^1.11.0", "swagger-ui-express": "^5.0.1", "ws": "^8.19.0", "zustand": "^5.0.0" @@ -1929,25 +1929,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/baseline-browser-mapping": { "version": "2.10.0", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", @@ -1960,16 +1941,6 @@ "node": ">=6.0.0" } }, - "node_modules/better-sqlite3": { - "version": "11.10.0", - "resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz", - "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1982,24 +1953,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", @@ -2091,29 +2044,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", @@ -2261,11 +2191,6 @@ "node": ">= 6" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -2407,28 +2332,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", @@ -2452,14 +2355,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2531,14 +2426,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2805,14 +2692,6 @@ "node": ">= 0.6" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", @@ -2948,11 +2827,6 @@ "node": ">=16.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", @@ -3059,11 +2933,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -3143,11 +3012,6 @@ "node": ">=0.10.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, "node_modules/gl-matrix": { "version": "3.4.4", "resolved": "https://registry.npmmirror.com/gl-matrix/-/gl-matrix-3.4.4.tgz", @@ -3254,25 +3118,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", @@ -3312,11 +3157,6 @@ "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3714,17 +3554,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", @@ -3745,11 +3574,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -3789,11 +3613,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3808,28 +3627,6 @@ "node": ">= 0.6" } }, - "node_modules/node-abi": { - "version": "3.87.0", - "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.87.0.tgz", - "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", @@ -3884,14 +3681,6 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -4196,32 +3985,6 @@ "resolved": "https://registry.npmmirror.com/potpack/-/potpack-2.1.0.tgz", "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==" }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4248,15 +4011,6 @@ "node": ">= 0.10" } }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", @@ -4327,28 +4081,6 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", @@ -4461,19 +4193,6 @@ "pify": "^2.3.0" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", @@ -4814,49 +4533,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/size-sensor": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/size-sensor/-/size-sensor-1.0.3.tgz", @@ -4942,6 +4618,11 @@ "node": ">=0.10.0" } }, + "node_modules/sql.js": { + "version": "1.14.0", + "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.0.tgz", + "integrity": "sha512-NXYh+kFqLiYRCNAaHD0PcbjFgXyjuolEKLMk5vRt2DgPENtF1kkNzzMlg42dUk5wIsH8MhUzsRhaUxIisoSlZQ==" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", @@ -4950,14 +4631,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5083,32 +4756,6 @@ "node": ">=14.0.0" } }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", @@ -5223,17 +4870,6 @@ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", @@ -5371,7 +5007,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/utils-merge": { "version": "1.0.1", @@ -5472,11 +5109,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", diff --git a/package.json b/package.json index 07e3cc5..6e3c9d0 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "verify:full": "./scripts/verify-pipeline.sh --start-crawler" }, "dependencies": { - "better-sqlite3": "^11.6.0", "cors": "^2.8.5", "echarts": "^5.5.0", "echarts-for-react": "^3.0.2", @@ -31,6 +30,7 @@ "react-dom": "^18.3.1", "react-map-gl": "^7.1.7", "react-router-dom": "^7.13.1", + "sql.js": "^1.11.0", "swagger-ui-express": "^5.0.1", "ws": "^8.19.0", "zustand": "^5.0.0" diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..9c9895c --- /dev/null +++ b/server/README.md @@ -0,0 +1,170 @@ +# 后端运行逻辑 + +后端是 **Node.js Express + SQLite + WebSocket**,与 Python 爬虫共用同一数据库文件,负责提供「态势数据」API、实时推送和简单统计。 + +--- + +## 一、启动方式 + +```bash +npm run api # 启动 server/index.js,默认端口 3001 +``` + +- 端口:`process.env.API_PORT || 3001` +- 数据库:`process.env.DB_PATH` 或 `server/data.db`(与爬虫共用) + +--- + +## 二、整体架构 + +``` + ┌─────────────────────────────────────────┐ + │ server/index.js │ + │ (HTTP Server + WebSocket Server) │ + └─────────────────────────────────────────┘ + │ + ┌───────────────────────────────┼───────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ /api/* │ │ /ws │ │ 静态 dist │ + │ routes.js │ │ WebSocket │ │ (生产) │ + └──────┬──────┘ └──────┬──────┘ └─────────────┘ + │ │ + │ 读/写 │ 广播 situation + stats + ▼ │ + ┌─────────────┐ │ + │ db.js │◄─────────────────────┘ + │ (SQLite) │ getSituation() / getStats() + └──────┬──────┘ + │ + │ 同文件 data.db + ▼ + ┌─────────────┐ + │ Python 爬虫 │ 抓取 → 去重 → AI 清洗 → 映射到库字段 → 写表 → POST /api/crawler/notify + │ situation_ │ (main.py 或 gdelt 服务;写 situation_update / news_content / combat_losses 等) + │ update 等 │ + └─────────────┘ +``` + +--- + +## 三、核心模块 + +| 文件 | 作用 | +|------|------| +| **index.js** | 创建 HTTP + WebSocket 服务,挂载路由、静态资源、定时广播、爬虫通知回调 | +| **routes.js** | 所有 `/api/*` 接口:situation、db/dashboard、visit、feedback、share、stats、events、news 等 | +| **situationData.js** | `getSituation()`:从多张表聚合为前端所需的「态势」JSON(军力、基地、战损、事件脉络、GDELT 等) | +| **db.js** | SQLite 连接、建表、迁移(better-sqlite3,WAL 模式) | +| **stats.js** | `getStats()`:在看人数、累计访问、留言数、分享数 | +| **openapi.js** | Swagger/OpenAPI 文档定义 | +| **seed.js** | 初始化/重置种子数据(可单独运行 `npm run api:seed`) | + +--- + +## 四、数据流(读) + +1. **前端要「整页态势」** + - 请求 `GET /api/situation` → `routes.js` 调用 `getSituation()` + - `situationData.js` 从 db 读:`force_summary`、`power_index`、`force_asset`、`key_location`、`combat_losses`、`wall_street_trend`、`retaliation_*`、`situation_update`(最近 50 条)、`gdelt_events`、`conflict_stats` 等 + - 组装成 `{ lastUpdated, usForces, iranForces, recentUpdates, conflictEvents, conflictStats, civilianCasualtiesTotal }` 返回。 + +2. **前端要「事件列表」** + - `GET /api/events` 返回 `conflictEvents` + `conflict_stats` + `updated_at`(同样来自 getSituation 的数据)。 + +3. **前端要「原始表数据」** + - `GET /api/db/dashboard` 返回多张表的 `SELECT *` 结果(含 `situation_update`),供 `/db` 调试页使用。 + +4. **WebSocket** + - 连接 `ws://host/ws` 时立即收到一条 `{ type: 'situation', data: getSituation(), stats: getStats() }`。 + - 之后每 3 秒服务端主动广播同结构数据,前端可据此做实时刷新。 + +--- + +## 五、数据流(写) + +### 5.1 爬虫侧写库链路(推荐理解顺序) + +爬虫写入前端库的完整链路如下,**不是**「抓完直接写表」,而是经过去重、AI 清洗、字段映射后再落库: + +1. **爬虫抓取实时数据** + - RSS 等源抓取(`scrapers/rss_scraper.fetch_all`),得到原始条目列表。 + +2. **数据去重** + - 抓取阶段:RSS 内按 (title, url) 去重。 + - 落库前:按 `content_hash(title, summary, url)` 在 `news_content` 表中去重,仅**未出现过**的条目进入后续流程(`news_storage.save_and_dedup`)。 + +3. **去重后按批次推送给 AI 清洗** + - 对通过去重的每条/每批数据: + - **展示用清洗**:标题/摘要翻译、`clean_news_for_panel` 提炼为符合面板的纯文本与长度(如 summary ≤120 字),`ensure_category` / `ensure_severity` 规范为前端枚举(`cleaner_ai`)。 + - **结构化提取**(可选):`extractor_ai` / `extractor_dashscope` / `extractor_rules` 从新闻文本中抽取战损、基地状态等,输出符合 `panel_schema` 的结构。 + - 得到「有效数据」:既有人读的 summary/category/severity,也有可落库的 combat_losses_delta、key_location 等。 + +4. **有效数据映射回前端数据库字段** + - 事件脉络:清洗后的条目写入 `situation_update`(`db_writer.write_updates`)。 + - 资讯存档:去重后的新数据写入 `news_content`(已在步骤 2 完成)。 + - 结构化数据:AI 提取结果通过 `db_merge.merge` 映射到前端表结构,更新 `combat_losses`、`key_location`、`retaliation_*`、`wall_street_trend` 等(与 `situationData.getSituation` 所用字段一致)。 + +5. **更新数据库表并通知后端** + - 上述表更新完成后,爬虫请求 **POST /api/crawler/notify**。 + - 后端(index.js)更新 `situation.updated_at` 并调用 `broadcastSituation()`,前端通过 WebSocket 拿到最新态势。 + +实现上,**gdelt 服务**(`realtime_conflict_service`)里:先对抓取结果做翻译与清洗,再 `save_and_dedup` 去重落库 `news_content`,用去重后的新项写 `situation_update`,再按批次对这批新项做 AI 提取并 `db_merge.merge` 写战损/基地等表。 + +### 5.2 用户行为写入 + +- **POST /api/visit**:记 IP 到 `visits`,`visitor_count.total` +1,并触发一次广播。 +- **POST /api/feedback**:插入 `feedback`。 +- **POST /api/share**:`share_count.total` +1。 + +这些写操作在 `routes.js` 中通过 `db.prepare().run()` 完成。 + +--- + +## 六、API 一览 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | /api/health | 健康检查 | +| GET | /api/situation | 完整态势(供主面板) | +| GET | /api/events | 冲突事件 + 统计 | +| GET | /api/db/dashboard | 各表原始数据(供 /db 页) | +| GET | /api/news | 资讯列表(news_content 表) | +| GET | /api/stats | 在看/累计/留言/分享数 | +| POST | /api/visit | 记录访问并返回 stats | +| POST | /api/feedback | 提交留言 | +| POST | /api/share | 分享计数 +1 | +| POST | /api/crawler/notify | 爬虫通知:更新 updated_at 并广播(内部用) | + +- **Swagger**:`http://localhost:3001/api-docs` + +--- + +## 七、WebSocket 行为 + +- **路径**:`/ws`(与 HTTP 同端口)。 +- **连接时**:服务端发送一条 `{ type: 'situation', data, stats }`。 +- **定时广播**:`setInterval(broadcastSituation, 3000)` 每 3 秒向所有已连接客户端推送最新 `getSituation()` + `getStats()`。 +- **爬虫通知**:POST `/api/crawler/notify` 会立即执行一次 `broadcastSituation()`,不必等 3 秒。 + +--- + +## 八、与爬虫的协作 + +- **共享 DB**:后端与爬虫都使用同一 `DB_PATH`(默认 `server/data.db`)。 +- **爬虫写库链路**:爬虫抓取 → 去重 → AI 清洗出有效数据 → 映射到前端库字段 → 更新 `situation_update`、`news_content`、`combat_losses`、`key_location`、`gdelt_events` 等表 → 调用 POST `/api/crawler/notify` 通知后端。 +- **后端角色**:只读这些表(`getSituation()` 等)并推送;不参与抓取、去重或 AI 清洗,不调度爬虫。 + +整体上,后端是「读库 + 聚合 + 推送」的服务;写库来自**爬虫(经过去重与 AI 清洗、字段映射后)**以及**用户行为**(访问/留言/分享)。 + +--- + +## 九、本地验证链路 + +1. **启动后端**:`npm run api`(默认 3001)。 +2. **检查读库**:`curl -s http://localhost:3001/api/situation` 应返回含 `lastUpdated`、`recentUpdates` 的 JSON。 +3. **检查写库与通知**:爬虫跑完流水线后会 POST `/api/crawler/notify`,后端会更新 `situation.updated_at` 并广播;可再请求 `/api/situation` 看 `lastUpdated` 是否更新。 +4. **查原始表**:浏览器打开 `http://localhost:3001/api/db/dashboard` 或前端 `/db` 页,查看 `situation_update`、`news_content` 等表。 + +爬虫侧完整验证步骤见 **crawler/README.md** 的「本地验证链路」;项目根目录可执行 `./scripts/verify-pipeline.sh` 做一键检查。 diff --git a/server/data.db b/server/data.db index 67ec2da80921d275aac9fe05dff5aec64c63982d..e458902b08c9f5f50113e9292c00e8e5ee557346 100644 GIT binary patch delta 2907 zcmeHJYj9J?72aLhST>SYGT6HEO9qp~HkPf2C0i;sHX+2|2iS4*0AgDP(;8&i_|d#5 zmbf=E7;GLVVJ9g=C?qrmN&!#lOeamqOq>4b6w+xs%}kk=KFFk*OmL@dW}1}Hv(`1G z%JhGK^v?Zs9^dZXqx)&kDt4nZcBAxAzPX(catfg!3J;goq_>mGMA^lyQ%;ko)8h$M z`$KMbrF-VK^T&Ltl9j^@Z0_nG4o8d5l>Fv`_2ptC?P4EW_WD04u+RLdD%+Z6HyU!N z9HCrl_#-b=7UWR8WTHG&P-3z#u*rCAq^qYle98R@5Bz(A8SBV4y+BN#ncm{R7JjwrhC=oRo(NEQ|VY1<)7jw34tjV zXPOfZ#vW!`5xgf&AuNfbDS++Llk5~Jtf;$$;ZpM}pS+nD+cwC%^} zY52x|oc9#m+>gnxf&TzTV=#W;A*PO^$%X3&s+c+g=Baw74#T@sxv*u*#nhc}WNHOd zhcNRXQwJeux-~bpXyyw=y*XpG`l^)E%(;;AW<7hm!|48L~C! zr9*FU#_HhBR}_CQ?&vD|R5%jp9vL0(>l^LH&*;_;lrEhvOfwW_ZtLsY+8f@oL+(t* z-y!^cE<8gSFU}O>ulCU*s}(mlV{$_q=PtR;4aSAeoWB)i|94I?WtS^9tIdd$aiTtV zY$Si2#+Xqy1)FWYi)713XY$;cj4M{|??@v)>UDelF1OF+_H=rE*rD+5RoYV-Pm<|T zHbBcYNB;U0-XnV(-d#!?#I8B~ttq@anB+GqEvZZ($vmz!!_{jJx^Cu!Yj&%#?QWqr zu+ZL=N%MuaDdAK`FSJGJftFj2{O%NfzR*Uc3u3n%{wGuT`9kZI&8duDXpOQ7uHJH# zZA|6m_s6?jR-R@#OZaiV*7A4D*%|BXd5hWC>Gp=I-63DF(j9=GeqOg2Grqf-+Kv6u z(Z0yh>5Ot%b0=4l)%TF$NZ;_Ni0@1<7cY-fy9H_gUD_|+8mFtcd@R37_^14@*b;Ak z&q{yA#g*5M`A3WC)c^a&|3u#qC#z_eQT{@TGQ30>tu!$+9eZ_wVYS*X{C2vAepT$U z)3x*~;vG9}r`v?0&?ed^!U}DqTg53EZ4ne~OqCbz;SO$mshQp)%gVAuLMSW77&%@E1#qB&?Vv|^tj|*-TNAfZG zg!sLT9v7|xj2;ua3b2qz#Z_s(EQ$-~mhZf0`DY6MbNRLEBjU|Mx{j$GBCCitGPPZ_ z7Rh*nc&UhDJ6|t;H+M9#M%{hA2U7 zK(rx(h+0Gqq8ibOcmfed^dp84Pa~d1#1NB+Lx|&uR}d!<4n!WJ2eBU^pGOxRZY-KO zK131h`^Se2+nj0hlC8jh86Tr$TAh>(Pp%bGGd$hCBt^q>EhJUL<1r{@!=txW>V|QR z6b=vHfK(1+zmyJ-%4(?{9>YFeJk5L0+x>!gnn#L4#Of}DCEI2CNOu#_=?>DY=I$v@bVSI9ZowgziYVTq#cP_Lt-{9F`J&4 QU67c~NX!~f&t^9K3sAf$ZU6uP delta 3446 zcmc&%d303O9e(%DlF4Ld?qnkqk}#76G6`AU>@$H#Ajp;kP-s=O32!zNnJi>E2_zwz z7n_iTfOyqg@f1`J*0Kpiq&-Dkq4p2OYHew2tsbSN)k=DfTdQJ!cQO&o|DL|{=DYW| z-0yyOoteRRt%L7c@5|EH3W9JDKCx%7)uXBv${Mmy1}Drq$41dnA$lrAcbRYUYx8-M zXVj)+1&%#U?NZCU!})(cVcciYpnCkVVW;a#HktgOJjJLb8bvzd8OBpp?~@#(Ivv5o z9mN&d`8uLb=5|*{{bs*(O8kttuQ$-^o)A)WHG=Lfa-QrXTgfV2FG(}}-SA7psG-4N zH>mZ0)IY3m)Vp;xlgSpdj@Cp(g*7Tr7O^Vz8A3T-A6ba2dg--DRUVAWoNmiR8G_K% zUh3rv{xB}wUNI^eoDMgs(wU!`6*R|Tw03B+zq8;=YU_bpsYZ3PK(HA4W5-q70Qb?78 zoFh+@!{n!=icm6221uB+k*R*X+WN z6YHkCw+Z)hClGrDIbQP!(RgRBfq2EaBVKXz$1CSl&q6#=zP@PmTfSt@PU1S}fl}*N%g=o<1@T?bgxDVCv62qZT&@TnH=*b%bCT9h5mV5v^;RIQp zrq?BF5W+~Kfw!w8YXA*m4WCwU;+eyw(9AXH;Al;IG_X6Q7KJ9soc@VPmr~3RR4sDQ1?11q2H%|lC39(ywn-HdWs zWwI(z%3+ zhZl3$vIS@ZN@ou(fLI!PdjVR8Qkl6BrGwK|h*p5JuMn*U=Y>M(A%&T&&@P!tR*+<5 z2f0PqM^=zzVBR9I^ekG0mLMH_p$L>EF=H{R)vp=S!r8A(NJuy5Al6ijJO-(=6OL;r zh^0vFP$D~C4A~m?bumyRuoVk|P|faN2u>Ayk6TJsS^}1W^_74S%wFRKVwTbvy>J8l zw$ks?ua*bcxl*(S%R8913@ycSKdUR_b{~7V48axH%l;bkZ)bVs2yUcpY->4>_poQn zQT0PcH1U7CeOD-P-HrDz*vr4~8<;(AM)gmKsA@6ax^sp70@+Vi8$LB0(O=cK>Q3uc zB#kBMw6`YyB+;a4NI0baP(7r2M-@??QhcGP#uMmGbi1&hCR_X?``ZwF@R|K>im=(9 zA;icb|j+=u>!77H?v)M}$1 zTY{r9Bif(CPA0)t)IW)HSm)1h&-_4pQ)g2^ibTsq$jFU`GF!SEeBQ863i)lppx5OW zT~3zVt1+?O!&u9H|8pEMHiv>uUCmKld8|ocDtnKUesL7%v5}*=&JtlwxI4B+-U?V)hMq4rL8=``-S zp{Jsmp0@IyE}em%mYn&{R9j;iU8&HM%`JJtUb`7RG3b%J9#<$3_Oa$ZSgzN{RVr5fE4)qLba!KP z@qEb7<>UV?r)b@!%3}x5-l(dmAjY_btCHZ$*d{u?PDjA)u?GWe;v9_Pt7ma*Oix*U zEVT=y=z;9j=OE~P4tK^DUY!>UX7M%h23!G;!|n{ZLqU(<7hvy!GS&2~)x3aLUZ1tX z+*qr-Q<;rZUgIOMyo6P(=SAEc<8@eK83SCO!|Aby{gTt}4Tb~WP?&9g86;eI32#e+ z`RXvMVXeD!n8c~t&O@m7L#3V-oX6Jdd&xa7#@EYr+FdTuHl`H_&4~r z%waEZrYCo+a>#~8sd2m1h#VR diff --git a/server/data.db-shm b/server/data.db-shm index 09a33a379d84a0c833cabb931e2be7eab1ce0e60..e1d2b9b9c871d6916884fda117b79e24c25f2396 100644 GIT binary patch delta 849 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 delta 187034 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 s.trim()).filter(Boolean) + statements.forEach((s) => nativeDb.run(s)) + persist() + }, + pragma(str) { + nativeDb.run('PRAGMA ' + str) + }, + } +} + +function runMigrations(db) { + const exec = (sql) => db.exec(sql) + const prepare = (sql) => db.prepare(sql) + + exec(` CREATE TABLE IF NOT EXISTS situation ( id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); - CREATE TABLE IF NOT EXISTS force_summary ( side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')), total_assets INTEGER NOT NULL, @@ -26,7 +73,6 @@ db.exec(` missile_consumed INTEGER NOT NULL, missile_stock INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS power_index ( side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')), overall INTEGER NOT NULL, @@ -34,7 +80,6 @@ db.exec(` economic_power INTEGER NOT NULL, geopolitical_influence INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS force_asset ( id TEXT PRIMARY KEY, side TEXT NOT NULL CHECK (side IN ('us', 'iran')), @@ -45,7 +90,6 @@ db.exec(` lat REAL, lng REAL ); - CREATE TABLE IF NOT EXISTS key_location ( id INTEGER PRIMARY KEY AUTOINCREMENT, side TEXT NOT NULL CHECK (side IN ('us', 'iran')), @@ -55,7 +99,6 @@ db.exec(` type TEXT, region TEXT ); - CREATE TABLE IF NOT EXISTS combat_losses ( side TEXT PRIMARY KEY CHECK (side IN ('us', 'iran')), bases_destroyed INTEGER NOT NULL, @@ -67,24 +110,20 @@ db.exec(` armor INTEGER NOT NULL, vehicles INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS wall_street_trend ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, value INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS retaliation_current ( id INTEGER PRIMARY KEY CHECK (id = 1), value INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS retaliation_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, value INTEGER NOT NULL ); - CREATE TABLE IF NOT EXISTS situation_update ( id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, @@ -92,7 +131,6 @@ db.exec(` summary TEXT NOT NULL, severity TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS gdelt_events ( event_id TEXT PRIMARY KEY, event_time TEXT NOT NULL, @@ -103,7 +141,6 @@ db.exec(` url TEXT, created_at TEXT DEFAULT (datetime('now')) ); - CREATE TABLE IF NOT EXISTS conflict_stats ( id INTEGER PRIMARY KEY CHECK (id = 1), total_events INTEGER NOT NULL DEFAULT 0, @@ -112,7 +149,6 @@ db.exec(` estimated_strike_count INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL ); - CREATE TABLE IF NOT EXISTS news_content ( id TEXT PRIMARY KEY, content_hash TEXT NOT NULL UNIQUE, @@ -125,57 +161,49 @@ db.exec(` severity TEXT NOT NULL DEFAULT 'medium', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); - CREATE INDEX IF NOT EXISTS idx_news_content_hash ON news_content(content_hash); - CREATE INDEX IF NOT EXISTS idx_news_content_published ON news_content(published_at DESC); -`) + `) + try { exec('CREATE INDEX IF NOT EXISTS idx_news_content_hash ON news_content(content_hash)') } catch (_) {} + try { exec('CREATE INDEX IF NOT EXISTS idx_news_content_published ON news_content(published_at DESC)') } catch (_) {} -// 迁移:为已有 key_location 表添加 type、region、status、damage_level 列 -try { - const cols = db.prepare('PRAGMA table_info(key_location)').all() - const names = cols.map((c) => c.name) - if (!names.includes('type')) db.exec('ALTER TABLE key_location ADD COLUMN type TEXT') - if (!names.includes('region')) db.exec('ALTER TABLE key_location ADD COLUMN region TEXT') - if (!names.includes('status')) db.exec('ALTER TABLE key_location ADD COLUMN status TEXT DEFAULT "operational"') - if (!names.includes('damage_level')) db.exec('ALTER TABLE key_location ADD COLUMN damage_level INTEGER') -} catch (_) {} -// 迁移:combat_losses 添加平民伤亡、updated_at -try { - const lossCols = db.prepare('PRAGMA table_info(combat_losses)').all() - const lossNames = lossCols.map((c) => c.name) - if (!lossNames.includes('civilian_killed')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_killed INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('civilian_wounded')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_wounded INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('updated_at')) db.exec('ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))') - if (!lossNames.includes('drones')) db.exec('ALTER TABLE combat_losses ADD COLUMN drones INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('missiles')) db.exec('ALTER TABLE combat_losses ADD COLUMN missiles INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('helicopters')) db.exec('ALTER TABLE combat_losses ADD COLUMN helicopters INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('submarines')) db.exec('ALTER TABLE combat_losses ADD COLUMN submarines INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('tanks')) db.exec('ALTER TABLE combat_losses ADD COLUMN tanks INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('carriers')) { - db.exec('ALTER TABLE combat_losses ADD COLUMN carriers INTEGER NOT NULL DEFAULT 0') - db.exec('UPDATE combat_losses SET carriers = tanks') - } - if (!lossNames.includes('civilian_ships')) db.exec('ALTER TABLE combat_losses ADD COLUMN civilian_ships INTEGER NOT NULL DEFAULT 0') - if (!lossNames.includes('airport_port')) db.exec('ALTER TABLE combat_losses ADD COLUMN airport_port INTEGER NOT NULL DEFAULT 0') -} catch (_) {} - -// 迁移:所有表添加 updated_at 用于数据回放 -const addUpdatedAt = (table) => { try { - const cols = db.prepare(`PRAGMA table_info(${table})`).all() - if (!cols.some((c) => c.name === 'updated_at')) { - db.exec(`ALTER TABLE ${table} ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))`) - } + const cols = prepare('PRAGMA table_info(key_location)').all() + const names = cols.map((c) => c.name) + if (!names.includes('type')) exec('ALTER TABLE key_location ADD COLUMN type TEXT') + if (!names.includes('region')) exec('ALTER TABLE key_location ADD COLUMN region TEXT') + if (!names.includes('status')) exec('ALTER TABLE key_location ADD COLUMN status TEXT DEFAULT "operational"') + if (!names.includes('damage_level')) exec('ALTER TABLE key_location ADD COLUMN damage_level INTEGER') + } catch (_) {} + try { + const lossCols = prepare('PRAGMA table_info(combat_losses)').all() + const lossNames = lossCols.map((c) => c.name) + if (!lossNames.includes('civilian_killed')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_killed INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('civilian_wounded')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_wounded INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('updated_at')) exec('ALTER TABLE combat_losses ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))') + if (!lossNames.includes('drones')) exec('ALTER TABLE combat_losses ADD COLUMN drones INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('missiles')) exec('ALTER TABLE combat_losses ADD COLUMN missiles INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('helicopters')) exec('ALTER TABLE combat_losses ADD COLUMN helicopters INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('submarines')) exec('ALTER TABLE combat_losses ADD COLUMN submarines INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('tanks')) exec('ALTER TABLE combat_losses ADD COLUMN tanks INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('carriers')) { + exec('ALTER TABLE combat_losses ADD COLUMN carriers INTEGER NOT NULL DEFAULT 0') + exec('UPDATE combat_losses SET carriers = tanks') + } + if (!lossNames.includes('civilian_ships')) exec('ALTER TABLE combat_losses ADD COLUMN civilian_ships INTEGER NOT NULL DEFAULT 0') + if (!lossNames.includes('airport_port')) exec('ALTER TABLE combat_losses ADD COLUMN airport_port INTEGER NOT NULL DEFAULT 0') } catch (_) {} -} -addUpdatedAt('force_summary') -addUpdatedAt('power_index') -addUpdatedAt('force_asset') -addUpdatedAt('key_location') -addUpdatedAt('retaliation_current') -// 来访统计:visits 用于在看(近期活跃 IP),visitor_count 用于累积人次(每次接入 +1) -try { - db.exec(` + const addUpdatedAt = (table) => { + try { + const cols = prepare(`PRAGMA table_info(${table})`).all() + if (!cols.some((c) => c.name === 'updated_at')) { + exec(`ALTER TABLE ${table} ADD COLUMN updated_at TEXT DEFAULT (datetime("now"))`) + } + } catch (_) {} + } + ;['force_summary', 'power_index', 'force_asset', 'key_location', 'retaliation_current'].forEach(addUpdatedAt) + + try { + exec(` CREATE TABLE IF NOT EXISTS visits ( ip TEXT PRIMARY KEY, last_seen TEXT NOT NULL DEFAULT (datetime('now')) @@ -185,30 +213,66 @@ try { total INTEGER NOT NULL DEFAULT 0 ); INSERT OR IGNORE INTO visitor_count (id, total) VALUES (1, 0); - `) -} catch (_) {} - -// 后台留言:供开发者收集用户反馈 -try { - db.exec(` + `) + } catch (_) {} + try { + exec(` CREATE TABLE IF NOT EXISTS feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, ip TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) - `) -} catch (_) {} - -// 分享次数:累计分享次数 -try { - db.exec(` + `) + } catch (_) {} + try { + exec(` CREATE TABLE IF NOT EXISTS share_count ( id INTEGER PRIMARY KEY CHECK (id = 1), total INTEGER NOT NULL DEFAULT 0 ); INSERT OR IGNORE INTO share_count (id, total) VALUES (1, 0); - `) -} catch (_) {} + `) + } catch (_) {} +} -module.exports = db +async function initDb() { + const initSqlJs = require('sql.js') + const SQL = await initSqlJs() + let data = new Uint8Array(0) + if (fs.existsSync(dbPath)) { + data = new Uint8Array(fs.readFileSync(dbPath)) + } + const nativeDb = new SQL.Database(data) + + function persist() { + try { + const buf = nativeDb.export() + fs.writeFileSync(dbPath, Buffer.from(buf)) + } catch (e) { + console.error('[db] persist error:', e.message) + } + } + + nativeDb.run('PRAGMA journal_mode = WAL') + const wrapped = wrapDatabase(nativeDb, persist) + runMigrations(wrapped) + _db = wrapped + return _db +} + +const proxy = { + prepare(sql) { + return getDb().prepare(sql) + }, + exec(sql) { + return getDb().exec(sql) + }, + pragma(str) { + getDb().pragma(str) + }, +} + +module.exports = proxy +module.exports.initDb = initDb +module.exports.getDb = getDb diff --git a/server/index.js b/server/index.js index 66350fa..b1132e9 100644 --- a/server/index.js +++ b/server/index.js @@ -4,6 +4,7 @@ const fs = require('fs') const express = require('express') const cors = require('cors') const { WebSocketServer } = require('ws') +const db = require('./db') const routes = require('./routes') const { getSituation } = require('./situationData') @@ -67,7 +68,12 @@ function notifyCrawlerUpdate() { } catch (_) {} } -server.listen(PORT, () => { - console.log(`API + WebSocket running at http://localhost:${PORT}`) - console.log(`Swagger docs at http://localhost:${PORT}/api-docs`) +db.initDb().then(() => { + server.listen(PORT, () => { + console.log(`API + WebSocket running at http://localhost:${PORT}`) + console.log(`Swagger docs at http://localhost:${PORT}/api-docs`) + }) +}).catch((err) => { + console.error('DB init failed:', err) + process.exit(1) }) diff --git a/server/seed.js b/server/seed.js index e3b4a1c..2c96344 100644 --- a/server/seed.js +++ b/server/seed.js @@ -186,4 +186,7 @@ function seed() { console.log('Seed completed.') } -seed() +require('./db').initDb().then(() => seed()).catch((err) => { + console.error('Seed failed:', err) + process.exit(1) +}) diff --git a/server/situationData.js b/server/situationData.js index 92b1b5f..85b5d95 100644 --- a/server/situationData.js +++ b/server/situationData.js @@ -155,6 +155,8 @@ function getSituation() { })), conflictStats, civilianCasualtiesTotal, + // 顶层聚合,便于 sit.combatLosses.us / sit.combatLosses.iran 与 usForces/iranForces 内保持一致 + combatLosses: { us: usLosses, iran: irLosses }, } }