From 2dc6bf16ec78580ff4f080efa9e123e76f843525 Mon Sep 17 00:00:00 2001 From: ZHENG XIAOYI Date: Sat, 7 Feb 2026 02:26:00 +0800 Subject: [PATCH] refactor: migrate database layer to Prisma ORM --- .gitignore | 2 + .npmrc | 1 - package.json | 4 +- pnpm-lock.yaml | 437 ++++++++++++++++------------- prisma.config.ts | 15 + prisma/schema.prisma | 42 +++ scripts/init-db.ts | 99 ++----- src/auth/auth.guard.ts | 55 ++-- src/auth/auth.service.ts | 120 ++++---- src/companies/companies.service.ts | 431 ++++++++++++++-------------- src/database/database.service.ts | 58 +--- src/database/database.ts | 56 ---- src/serials/serials.service.ts | 294 +++++++++++-------- src/types/index.d.ts | 59 ++-- src/utils/database.ts | 56 ---- 15 files changed, 875 insertions(+), 854 deletions(-) create mode 100644 prisma.config.ts create mode 100644 prisma/schema.prisma delete mode 100644 src/database/database.ts delete mode 100644 src/utils/database.ts diff --git a/.gitignore b/.gitignore index badef44..4e142ec 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,5 @@ dist/ # NestJS .nest-cli.json + +/generated/prisma diff --git a/.npmrc b/.npmrc index 497dd9a..5d4f6f6 100644 --- a/.npmrc +++ b/.npmrc @@ -1,4 +1,3 @@ -only-built-dependencies[]=better-sqlite3 only-built-dependencies[]=esbuild only-built-dependencies[]=@nestjs/core only-built-dependencies[]=unrs-resolver \ No newline at end of file diff --git a/package.json b/package.json index 60cea33..f67e149 100644 --- a/package.json +++ b/package.json @@ -28,14 +28,15 @@ "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.1.13", "@nestjs/serve-static": "^5.0.4", + "@prisma/client": "^6.19.2", "bcryptjs": "^3.0.3", - "better-sqlite3": "^12.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv": "^17.2.3", "jsonwebtoken": "^9.0.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", + "prisma": "^6.19.2", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2" @@ -44,7 +45,6 @@ "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.1.13", - "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3301449..a14bdbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,12 +29,12 @@ importers: '@nestjs/serve-static': specifier: ^5.0.4 version: 5.0.4(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(express@5.2.1) + '@prisma/client': + specifier: ^6.19.2 + version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) bcryptjs: specifier: ^3.0.3 version: 3.0.3 - better-sqlite3: - specifier: ^12.6.2 - version: 12.6.2 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -53,6 +53,9 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 + prisma: + specifier: ^6.19.2 + version: 6.19.2(typescript@5.9.3) qrcode: specifier: ^1.5.4 version: 1.5.4 @@ -72,9 +75,6 @@ importers: '@nestjs/testing': specifier: ^11.1.13 version: 11.1.13(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.13)(@nestjs/platform-express@11.1.13) - '@types/better-sqlite3': - specifier: ^7.6.13 - version: 7.6.13 '@types/express': specifier: ^5.0.6 version: 5.0.6 @@ -896,6 +896,36 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@prisma/client@6.19.2': + resolution: {integrity: sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.2': + resolution: {integrity: sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==} + + '@prisma/debug@6.19.2': + resolution: {integrity: sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': + resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} + + '@prisma/engines@6.19.2': + resolution: {integrity: sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==} + + '@prisma/fetch-engine@6.19.2': + resolution: {integrity: sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==} + + '@prisma/get-platform@6.19.2': + resolution: {integrity: sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==} + '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} @@ -905,6 +935,9 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -939,9 +972,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/better-sqlite3@7.6.13': - resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -1334,13 +1364,6 @@ packages: resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==} hasBin: true - better-sqlite3@12.6.2: - resolution: {integrity: sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA==} - engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1387,6 +1410,14 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1425,9 +1456,6 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -1436,6 +1464,12 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -1511,6 +1545,9 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -1573,10 +1610,6 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - dedent@1.7.1: resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: @@ -1585,9 +1618,9 @@ packages: babel-plugin-macros: optional: true - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -1596,6 +1629,9 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1604,9 +1640,8 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} @@ -1651,6 +1686,9 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + electron-to-chromium@1.5.286: resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} @@ -1664,13 +1702,14 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} @@ -1750,10 +1789,6 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - expect@30.2.0: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -1762,6 +1797,13 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1781,9 +1823,6 @@ packages: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1823,9 +1862,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -1871,8 +1907,9 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -1956,9 +1993,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2155,6 +2189,10 @@ packages: node-notifier: optional: true + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2333,10 +2371,6 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - minimatch@10.1.2: resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==} engines: {node: 20 || >=22} @@ -2355,9 +2389,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -2373,9 +2404,6 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} - napi-build-utils@2.0.0: - resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2391,16 +2419,15 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-abi@3.87.0: - resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} - engines: {node: '>=10'} - node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} node-emoji@1.11.0: resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} @@ -2415,6 +2442,11 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + nypm@0.6.5: + resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2423,6 +2455,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2507,9 +2542,15 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2533,6 +2574,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2541,11 +2585,6 @@ packages: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} - prebuild-install@7.1.3: - resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} - engines: {node: '>=10'} - hasBin: true - prettier@3.8.1: resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} @@ -2555,17 +2594,27 @@ packages: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + prisma@6.19.2: + resolution: {integrity: sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - pump@3.0.3: - resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} @@ -2589,9 +2638,8 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2718,12 +2766,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2796,10 +2838,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2836,13 +2874,6 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - tar-fs@2.1.4: - resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} - terser-webpack-plugin@5.3.16: resolution: {integrity: sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==} engines: {node: '>= 10.13.0'} @@ -2868,6 +2899,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -2947,9 +2982,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -3994,6 +4026,41 @@ snapshots: '@pkgr/core@0.2.9': {} + '@prisma/client@6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)': + optionalDependencies: + prisma: 6.19.2(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@6.19.2': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.2': {} + + '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} + + '@prisma/engines@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/fetch-engine': 6.19.2 + '@prisma/get-platform': 6.19.2 + + '@prisma/fetch-engine@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 + '@prisma/get-platform': 6.19.2 + + '@prisma/get-platform@6.19.2': + dependencies: + '@prisma/debug': 6.19.2 + '@sinclair/typebox@0.34.48': {} '@sinonjs/commons@3.0.1': @@ -4004,6 +4071,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -4047,10 +4116,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/better-sqlite3@7.6.13': - dependencies: - '@types/node': 25.2.1 - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -4460,15 +4525,6 @@ snapshots: bcryptjs@3.0.3: {} - better-sqlite3@12.6.2: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.3 - - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - bl@4.1.0: dependencies: buffer: 5.7.1 @@ -4533,6 +4589,21 @@ snapshots: bytes@3.1.2: {} + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -4564,12 +4635,16 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@1.1.4: {} - chrome-trace-event@1.0.4: {} ci-info@4.4.0: {} + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} + cjs-module-lexer@2.2.0: {} class-transformer@0.5.1: {} @@ -4643,6 +4718,8 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + confbox@0.2.4: {} + consola@3.4.2: {} content-disposition@1.0.1: {} @@ -4687,13 +4764,9 @@ snapshots: decamelize@1.2.0: {} - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - dedent@1.7.1: {} - deep-extend@0.6.0: {} + deepmerge-ts@7.1.5: {} deepmerge@4.3.1: {} @@ -4701,11 +4774,13 @@ snapshots: dependencies: clone: 1.0.4 + defu@6.1.4: {} + delayed-stream@1.0.0: {} depd@2.0.0: {} - detect-libc@2.1.2: {} + destr@2.0.5: {} detect-newline@3.1.0: {} @@ -4742,6 +4817,11 @@ snapshots: ee-first@1.1.1: {} + effect@3.18.4: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + electron-to-chromium@1.5.286: {} emittery@0.13.1: {} @@ -4750,11 +4830,9 @@ snapshots: emoji-regex@9.2.2: {} - encodeurl@2.0.0: {} + empathic@2.0.0: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 + encodeurl@2.0.0: {} enhanced-resolve@5.19.0: dependencies: @@ -4850,8 +4928,6 @@ snapshots: exit-x@0.2.2: {} - expand-template@2.0.3: {} - expect@30.2.0: dependencies: '@jest/expect-utils': 30.2.0 @@ -4894,6 +4970,12 @@ snapshots: transitivePeerDependencies: - supports-color + exsolve@1.0.8: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4915,8 +4997,6 @@ snapshots: transitivePeerDependencies: - supports-color - file-uri-to-path@1.0.0: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4977,8 +5057,6 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} - fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -5024,7 +5102,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - github-from-package@0.0.0: {} + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.5 + pathe: 2.0.3 glob-to-regexp@0.4.1: {} @@ -5114,8 +5199,6 @@ snapshots: inherits@2.0.4: {} - ini@1.3.8: {} - ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} @@ -5493,6 +5576,8 @@ snapshots: - supports-color - ts-node + jiti@2.6.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -5642,8 +5727,6 @@ snapshots: mimic-fn@2.1.0: {} - mimic-response@3.1.0: {} - minimatch@10.1.2: dependencies: '@isaacs/brace-expansion': 5.0.1 @@ -5660,8 +5743,6 @@ snapshots: minipass@7.1.2: {} - mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -5680,8 +5761,6 @@ snapshots: mute-stream@2.0.0: {} - napi-build-utils@2.0.0: {} - napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} @@ -5690,16 +5769,14 @@ snapshots: neo-async@2.6.2: {} - node-abi@3.87.0: - dependencies: - semver: 7.7.4 - node-abort-controller@3.1.1: {} node-emoji@1.11.0: dependencies: lodash: 4.17.23 + node-fetch-native@1.6.7: {} + node-int64@0.4.0: {} node-releases@2.0.27: {} @@ -5710,10 +5787,18 @@ snapshots: dependencies: path-key: 3.1.1 + nypm@0.6.5: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + object-assign@4.1.1: {} object-inspect@1.13.4: {} + ohash@2.0.11: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -5800,8 +5885,12 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + pause@0.0.1: {} + perfect-debounce@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5816,25 +5905,16 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + pluralize@8.0.0: {} pngjs@5.0.0: {} - prebuild-install@7.1.3: - dependencies: - detect-libc: 2.1.2 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 2.0.0 - node-abi: 3.87.0 - pump: 3.0.3 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.4 - tunnel-agent: 0.6.0 - prettier@3.8.1: {} pretty-format@30.2.0: @@ -5843,18 +5923,24 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + prisma@6.19.2(typescript@5.9.3): + dependencies: + '@prisma/config': 6.19.2 + '@prisma/engines': 6.19.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - pump@3.0.3: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - punycode@2.3.1: {} + pure-rand@6.1.0: {} + pure-rand@7.0.1: {} qrcode@1.5.4: @@ -5880,12 +5966,10 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - rc@1.2.8: + rc9@2.1.2: dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 + defu: 6.1.4 + destr: 2.0.5 react-is@18.3.1: {} @@ -6030,14 +6114,6 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - slash@3.0.0: {} source-map-support@0.5.13: @@ -6101,8 +6177,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} strtok3@10.3.4: @@ -6147,21 +6221,6 @@ snapshots: tapable@2.3.0: {} - tar-fs@2.1.4: - dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.3 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.5 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 - terser-webpack-plugin@5.3.16(webpack@5.104.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -6184,6 +6243,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + tinyexec@1.0.2: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -6268,10 +6329,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - tunnel-agent@0.6.0: - dependencies: - safe-buffer: 5.2.1 - type-detect@4.0.8: {} type-fest@0.21.3: {} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..74de320 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,15 @@ +import "dotenv/config"; +import { defineConfig } from "prisma/config"; +import path from "path"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: + process.env["DATABASE_URL"] || + "file:" + path.join(process.cwd(), "data/database.sqlite"), + }, +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..727bacb --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,42 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + username String @unique + password String + name String + email String? + role String @default("user") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + serials Serial[] +} + +model Company { + id Int @id @default(autoincrement()) + companyName String @unique + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + serials Serial[] +} + +model Serial { + id Int @id @default(autoincrement()) + serialNumber String @unique + companyName String + validUntil DateTime? + isActive Boolean @default(true) + createdBy Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User? @relation(fields: [createdBy], references: [id]) + company Company? @relation(fields: [companyName], references: [companyName]) +} diff --git a/scripts/init-db.ts b/scripts/init-db.ts index 5d11b5e..f7415cf 100644 --- a/scripts/init-db.ts +++ b/scripts/init-db.ts @@ -1,91 +1,44 @@ -import Database from 'better-sqlite3'; -import bcrypt from 'bcryptjs'; -import path from 'path'; -import fs from 'fs'; +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; -const dbPath = path.join(process.cwd(), 'data/database.sqlite'); -const dbDir = path.dirname(dbPath); - -if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); -} - -const db = new Database(dbPath, { verbose: console.log }); - -const createTables = (): void => { - db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, - name TEXT NOT NULL, - email TEXT, - role TEXT DEFAULT 'user', - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS companies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - company_name TEXT UNIQUE NOT NULL, - is_active BOOLEAN BOOLEAN DEFAULT 1, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - db.exec(` - CREATE TABLE IF NOT EXISTS serials ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - serial_number TEXT UNIQUE NOT NULL, - company_name TEXT NOT NULL, - valid_until DATETIME, - is_active BOOLEAN DEFAULT 1, - created_by INTEGER, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (created_by) REFERENCES users (id), - FOREIGN KEY (company_name) REFERENCES companies (company_name) - ) - `); - - db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_companies ON companies (company_name)'); - db.exec('CREATE INDEX IF NOT EXISTS idx_serial_number ON serials (serial_number)'); - db.exec('CREATE INDEX IF NOT EXISTS idx_company_name_serials ON serials (company_name)'); - db.exec('CREATE INDEX IF NOT EXISTS idx_created_by ON serials (created_by)'); - - console.log('数据库表创建完成'); -}; +const prisma = new PrismaClient({ + log: ["query", "error", "warn"], +}); const createDefaultUser = async (): Promise => { - const username = 'admin'; - const password = 'Beifan@2026'; - const name = '系统管理员'; + const username = "admin"; + const password = "Beifan@2026"; + const name = "系统管理员"; - const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); + const user = await prisma.user.findUnique({ + where: { username }, + }); if (!user) { const hashedPassword = await bcrypt.hash(password, 10); - db.prepare( - 'INSERT INTO users (username, password, name, email, role) VALUES (?, ?, ?, ?, ?)' - ).run(username, hashedPassword, name, 'admin@example.com', 'admin'); + await prisma.user.create({ + data: { + username, + password: hashedPassword, + name, + email: "admin@example.com", + role: "admin", + }, + }); - console.log('默认管理员用户创建完成:'); - console.log('用户名:', username); - console.log('密码:', password); + console.log("默认管理员用户创建完成:"); + console.log("用户名:", username); + console.log("密码:", password); } else { - console.log('默认管理员用户已存在'); + console.log("默认管理员用户已存在"); } }; const initDatabase = async (): Promise => { - createTables(); await createDefaultUser(); - db.close(); - console.log('数据库连接已关闭'); + await prisma.$disconnect(); + console.log("数据库连接已关闭"); }; initDatabase(); diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 9d49ae3..3524ea0 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -1,7 +1,12 @@ -import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import { DatabaseService } from '../database/database.service'; -import { AuthUser } from '../types'; +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import { DatabaseService } from "../database/database.service"; +import { AuthUser } from "../types"; @Injectable() export class AuthGuard implements CanActivate { @@ -12,32 +17,42 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); - const authHeader = request.headers['authorization']; - const token = authHeader && authHeader.split(' ')[1]; + const authHeader = request.headers["authorization"]; + const token = authHeader && authHeader.split(" ")[1]; if (!token) { - throw new UnauthorizedException('访问令牌缺失'); + throw new UnauthorizedException("访问令牌缺失"); } try { - const decoded = this.jwtService.verify(token) as { userId: number; username: string; role: string }; - - const user = await this.dbService.get( - 'SELECT id, username, name, role FROM users WHERE id = ?', - [decoded.userId] - ); - + const decoded = this.jwtService.verify(token) as { + userId: number; + username: string; + role: string; + }; + + const prisma = this.dbService.getPrisma(); + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + username: true, + name: true, + role: true, + }, + }); + if (!user) { - throw new UnauthorizedException('用户不存在'); + throw new UnauthorizedException("用户不存在"); } - - request.user = user; + + request.user = user as AuthUser; return true; } catch (error) { - if (error.name === 'TokenExpiredError') { - throw new UnauthorizedException('令牌已过期'); + if (error.name === "TokenExpiredError") { + throw new UnauthorizedException("令牌已过期"); } - throw new UnauthorizedException('无效的令牌'); + throw new UnauthorizedException("无效的令牌"); } } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index b5eb1cd..bf403f8 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,8 +1,8 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { JwtService } from '@nestjs/jwt'; -import * as bcrypt from 'bcryptjs'; -import { DatabaseService } from '../database/database.service'; -import { User, AuthUser } from '../types'; +import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { JwtService } from "@nestjs/jwt"; +import * as bcrypt from "bcryptjs"; +import { DatabaseService } from "../database/database.service"; +import { User, AuthUser } from "../types"; @Injectable() export class AuthService { @@ -12,23 +12,30 @@ export class AuthService { ) {} async validateUser(username: string, password: string) { - const user = await this.dbService.get('SELECT * FROM users WHERE username = ?', [username]); + const prisma = this.dbService.getPrisma(); + const user = await prisma.user.findUnique({ + where: { username }, + }); if (!user) { - throw new UnauthorizedException('用户名或密码错误'); + throw new UnauthorizedException("用户名或密码错误"); } const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { - throw new UnauthorizedException('用户名或密码错误'); + throw new UnauthorizedException("用户名或密码错误"); } - return user; + return user as User; } async login(user: User) { - const payload = { userId: user.id, username: user.username, role: user.role }; + const payload = { + userId: user.id, + username: user.username, + role: user.role, + }; const token = this.jwtService.sign(payload); return { @@ -39,74 +46,81 @@ export class AuthService { name: user.name, email: user.email, role: user.role, - createdAt: user.created_at, + createdAt: user.createdAt, }, }; } async getProfile(userId: number) { - const user = await this.dbService.get( - 'SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', - [userId], - ); + const prisma = this.dbService.getPrisma(); + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { + id: true, + username: true, + name: true, + email: true, + role: true, + createdAt: true, + }, + }); if (!user) { - throw new UnauthorizedException('用户不存在'); + throw new UnauthorizedException("用户不存在"); } - return { - id: user.id, - username: user.username, - name: user.name, - email: user.email, - role: user.role, - createdAt: user.created_at, - }; + return user; } - async changePassword(userId: number, currentPassword: string, newPassword: string) { - const user = await this.dbService.get>('SELECT password FROM users WHERE id = ?', [ - userId, - ]); + async changePassword( + userId: number, + currentPassword: string, + newPassword: string, + ) { + const prisma = this.dbService.getPrisma(); + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { password: true }, + }); if (!user) { - throw new UnauthorizedException('用户不存在'); + throw new UnauthorizedException("用户不存在"); } - const isValidPassword = await bcrypt.compare(currentPassword, user.password); + const isValidPassword = await bcrypt.compare( + currentPassword, + user.password, + ); if (!isValidPassword) { - throw new UnauthorizedException('当前密码错误'); + throw new UnauthorizedException("当前密码错误"); } const hashedPassword = await bcrypt.hash(newPassword, 10); - await this.dbService.run('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [ - hashedPassword, - userId, - ]); + await prisma.user.update({ + where: { id: userId }, + data: { password: hashedPassword }, + }); - return { message: '密码修改成功' }; + return { message: "密码修改成功" }; } async updateProfile(userId: number, name: string, email: string | null) { - await this.dbService.run( - 'UPDATE users SET name = ?, email = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', - [name, email, userId], - ); + const prisma = this.dbService.getPrisma(); + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: { name, email }, + select: { + id: true, + username: true, + name: true, + email: true, + role: true, + createdAt: true, + }, + }); - const updatedUser = await this.dbService.get( - 'SELECT id, username, name, email, role, created_at FROM users WHERE id = ?', - [userId], - ); - - return { - id: updatedUser!.id, - username: updatedUser!.username, - name: updatedUser!.name, - email: updatedUser!.email, - role: updatedUser!.role, - createdAt: updatedUser!.created_at, - }; + return updatedUser; } } diff --git a/src/companies/companies.service.ts b/src/companies/companies.service.ts index e03d75b..e7a47b9 100644 --- a/src/companies/companies.service.ts +++ b/src/companies/companies.service.ts @@ -1,313 +1,324 @@ -import { Injectable } from '@nestjs/common'; -import { DatabaseService } from '../database/database.service'; +import { Injectable } from "@nestjs/common"; +import { DatabaseService } from "../database/database.service"; @Injectable() export class CompaniesService { constructor(private dbService: DatabaseService) {} async findAll(page: number, limit: number, search: string) { + const prisma = this.dbService.getPrisma(); const offset = (page - 1) * limit; - let query = `SELECT c.company_name, c.created_at as first_created, c.updated_at as last_created, c.is_active, (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name) as serial_count, (SELECT COUNT(*) FROM serials s WHERE s.company_name = c.company_name AND s.is_active = 1) as active_count FROM companies c`; - let countQuery = 'SELECT COUNT(*) as total FROM companies'; - let params: any[] = []; + const where = search + ? { + companyName: { contains: search }, + } + : undefined; - if (search) { - query += ' WHERE c.company_name LIKE ?'; - countQuery += ' WHERE company_name LIKE ?'; - params.push(`%${search}%`); - } - - query += ' ORDER BY c.updated_at DESC LIMIT ? OFFSET ?'; - params.push(parseInt(limit.toString()), parseInt(offset.toString())); - - const [companies, countResult] = await Promise.all([ - this.dbService.all(query, params), - this.dbService.get<{ total: number }>(countQuery, params.slice(0, -2)) + const [companies, total] = await Promise.all([ + prisma.company.findMany({ + where, + include: { + serials: true, + }, + orderBy: { updatedAt: "desc" }, + skip: offset, + take: limit, + }), + prisma.company.count({ where }), ]); - const total = countResult?.total || 0; const totalPages = Math.ceil(total / limit); return { - message: '获取企业列表成功', + message: "获取企业列表成功", data: companies.map((company: any) => ({ - companyName: company.company_name, - firstCreated: company.first_created, - lastCreated: company.last_created, - serialCount: company.serial_count, - activeCount: company.active_count, - status: company.is_active ? 'active' : 'disabled' + companyName: company.companyName, + firstCreated: company.createdAt, + lastCreated: company.updatedAt, + serialCount: company.serials.length, + activeCount: company.serials.filter((s: any) => s.isActive).length, + status: company.isActive ? "active" : "disabled", })), pagination: { page: parseInt(page.toString()), limit: parseInt(limit.toString()), total, - totalPages - } + totalPages, + }, }; } async findOne(companyName: string, page: number, limit: number) { + const prisma = this.dbService.getPrisma(); const offset = (page - 1) * limit; - const companyInfo = await this.dbService.get('SELECT * FROM companies WHERE company_name = ?', [companyName]); + const company = await prisma.company.findUnique({ + where: { companyName }, + include: { + serials: { + include: { + user: { + select: { name: true }, + }, + }, + orderBy: { createdAt: "desc" }, + }, + }, + }); - if (!companyInfo) { - throw new Error('企业不存在'); + if (!company) { + throw new Error("企业不存在"); } - const serialStats = await this.dbService.get(` - SELECT COUNT(*) as serial_count, - SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) as active_count, - SUM(CASE WHEN is_active = 0 THEN 1 ELSE 0 END) as disabled_count, - SUM(CASE WHEN valid_until IS NOT NULL AND valid_until <= datetime('now') THEN 1 ELSE 0 END) as expired_count - FROM serials - WHERE company_name = ? - `, [companyName]); + const now = new Date(); + const serialCount = company.serials.length; + const activeCount = company.serials.filter( + (s) => s.isActive && (!s.validUntil || s.validUntil > now), + ).length; + const disabledCount = company.serials.filter((s) => !s.isActive).length; + const expiredCount = company.serials.filter( + (s) => s.validUntil && s.validUntil <= now, + ).length; - const serials = await this.dbService.all(` - SELECT s.*, u.name as created_by_name - FROM serials s - LEFT JOIN users u ON s.created_by = u.id - WHERE s.company_name = ? - ORDER BY s.created_at DESC - LIMIT ? OFFSET ? - `, [companyName, parseInt(limit.toString()), parseInt(offset.toString())]); + const monthlyStatsMap = new Map(); - const stats = await this.dbService.all<{ month: string; count: number }>(` - SELECT strftime('%Y-%m', created_at) as month, - COUNT(*) as count - FROM serials - WHERE company_name = ? - GROUP BY strftime('%Y-%m', created_at) - ORDER BY month DESC - LIMIT 12 - `, [companyName]); + for (let i = 11; i >= 0; i--) { + const date = new Date(now.getFullYear(), now.getMonth() - i, 1); + const monthKey = date.toISOString().slice(0, 7); + const count = company.serials.filter((s) => { + const createdAt = new Date(s.createdAt); + return ( + createdAt.getFullYear() === date.getFullYear() && + createdAt.getMonth() === date.getMonth() + ); + }).length; + + if (count > 0) { + monthlyStatsMap.set(monthKey, count); + } + } + + const paginatedSerials = company.serials.slice(offset, offset + limit); return { - message: '获取企业详情成功', + message: "获取企业详情成功", data: { companyName: companyName, - serialCount: serialStats?.serial_count || 0, - activeCount: serialStats?.active_count || 0, - disabledCount: serialStats?.disabled_count || 0, - expiredCount: serialStats?.expired_count || 0, - firstCreated: (companyInfo as any).created_at, - lastCreated: (companyInfo as any).updated_at, - status: (companyInfo as any).is_active ? 'active' : 'disabled', - serials: serials.map((s: any) => ({ - serialNumber: s.serial_number, - validUntil: s.valid_until, - isActive: s.is_active, - createdAt: s.created_at, - createdBy: s.created_by_name + serialCount, + activeCount, + disabledCount, + expiredCount, + firstCreated: company.createdAt, + lastCreated: company.updatedAt, + status: company.isActive ? "active" : "disabled", + serials: paginatedSerials.map((s) => ({ + serialNumber: s.serialNumber, + validUntil: s.validUntil, + isActive: s.isActive, + createdAt: s.createdAt, + createdBy: s.user?.name, })), - monthlyStats: stats.map(stat => ({ - month: stat.month, - count: stat.count - })) - } + monthlyStats: Array.from(monthlyStatsMap.entries()).map( + ([month, count]) => ({ month, count }), + ), + }, }; } async update(companyName: string, newCompanyName: string) { - const existingCompany = await this.dbService.get( - 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', - [companyName] - ); + const prisma = this.dbService.getPrisma(); + const existingCompany = await prisma.serial.count({ + where: { companyName }, + }); - if (!existingCompany || (existingCompany as any).count === 0) { - throw new Error('企业不存在'); + if (existingCompany === 0) { + throw new Error("企业不存在"); } - const duplicateCompany = await this.dbService.get( - 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', - [newCompanyName] - ); + const duplicateCompany = await prisma.serial.count({ + where: { companyName: newCompanyName }, + }); - if (duplicateCompany && (duplicateCompany as any).count > 0) { - throw new Error('企业名称已存在'); + if (duplicateCompany > 0) { + throw new Error("企业名称已存在"); } - this.dbService.run( - 'UPDATE serials SET company_name = ?, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?', - [newCompanyName, companyName] - ); + await prisma.serial.updateMany({ + where: { companyName }, + data: { companyName: newCompanyName }, + }); return { - message: '企业名称更新成功', + message: "企业名称更新成功", data: { oldCompanyName: companyName, - newCompanyName - } + newCompanyName, + }, }; } async delete(companyName: string) { - const existingCompany = await this.dbService.get( - 'SELECT * FROM companies WHERE company_name = ?', - [companyName] - ); + const prisma = this.dbService.getPrisma(); + const existingCompany = await prisma.company.findUnique({ + where: { companyName }, + }); if (!existingCompany) { - throw new Error('企业不存在'); + throw new Error("企业不存在"); } - this.dbService.run('BEGIN TRANSACTION'); + const deleteResult = await prisma.$transaction(async (tx) => { + const serialDeleteCount = await tx.serial.deleteMany({ + where: { companyName }, + }); - try { - const serialDeleteResult = this.dbService.run( - 'DELETE FROM serials WHERE company_name = ?', - [companyName] - ); + await tx.company.delete({ + where: { companyName }, + }); - const companyDeleteResult = this.dbService.run( - 'DELETE FROM companies WHERE company_name = ?', - [companyName] - ); + return serialDeleteCount.count; + }); - if (companyDeleteResult.changes === 0) { - this.dbService.run('ROLLBACK'); - throw new Error('企业不存在'); - } - - this.dbService.run('COMMIT'); - - return { - message: '企业已完全删除,所有相关序列号已删除', - data: { - companyName: companyName, - deletedSerialCount: serialDeleteResult.changes, - deletedCompanyCount: companyDeleteResult.changes - } - }; - } catch (error) { - this.dbService.run('ROLLBACK'); - throw error; - } + return { + message: "企业已完全删除,所有相关序列号已删除", + data: { + companyName: companyName, + deletedSerialCount: deleteResult, + deletedCompanyCount: 1, + }, + }; } async deleteSerial(companyName: string, serialNumber: string) { - const serial = await this.dbService.get( - 'SELECT * FROM serials WHERE serial_number = ? AND company_name = ?', - [serialNumber.toUpperCase(), companyName] - ); + const prisma = this.dbService.getPrisma(); + const serial = await prisma.serial.findFirst({ + where: { + serialNumber: serialNumber.toUpperCase(), + companyName, + }, + }); if (!serial) { - throw new Error('序列号不存在或不属于该企业'); + throw new Error("序列号不存在或不属于该企业"); } - await this.dbService.run( - 'DELETE FROM serials WHERE serial_number = ? AND company_name = ?', - [serialNumber.toUpperCase(), companyName] - ); + await prisma.serial.delete({ + where: { serialNumber: serialNumber.toUpperCase() }, + }); return { - message: '序列号已成功删除', + message: "序列号已成功删除", data: { serialNumber: serialNumber.toUpperCase(), - companyName - } + companyName, + }, }; } async revoke(companyName: string) { - const existingCompany = await this.dbService.get( - 'SELECT COUNT(*) as count FROM serials WHERE company_name = ?', - [companyName] - ); + const prisma = this.dbService.getPrisma(); + const existingCompany = await prisma.serial.count({ + where: { companyName }, + }); - if (!existingCompany || (existingCompany as any).count === 0) { - throw new Error('企业不存在'); + if (existingCompany === 0) { + throw new Error("企业不存在"); } - await this.dbService.run( - 'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE company_name = ?', - [companyName] - ); + await prisma.serial.updateMany({ + where: { companyName }, + data: { isActive: false }, + }); return { - message: '企业已吊销,所有序列号已失效', + message: "企业已吊销,所有序列号已失效", data: { - companyName: companyName - } + companyName: companyName, + }, }; } async getStats() { - const companyCount = await this.dbService.get<{ count: number }>('SELECT COUNT(*) as count FROM companies'); - const serialCount = await this.dbService.get<{ count: number }>('SELECT COUNT(*) as count FROM serials'); - const activeCount = await this.dbService.get<{ count: number }>(` - SELECT COUNT(*) as count FROM serials - WHERE is_active = 1 AND (valid_until IS NULL OR valid_until > datetime('now')) - `); + const prisma = this.dbService.getPrisma(); + const now = new Date(); - const monthlyStats = await this.dbService.all<{ month: string; company_count: number; serial_count: number }>(` - SELECT strftime('%Y-%m', created_at) as month, - COUNT(DISTINCT company_name) as company_count, - COUNT(*) as serial_count - FROM serials - WHERE created_at >= strftime('%Y-%m-%d', datetime('now', '-12 months')) - GROUP BY strftime('%Y-%m', created_at) - ORDER BY month ASC - `); + const [companies, serials, recentCompanies, recentSerials] = + await Promise.all([ + prisma.company.findMany(), + prisma.serial.findMany({ + include: { + company: true, + }, + }), + prisma.company.findMany({ + orderBy: { updatedAt: "desc" }, + take: 10, + }), + prisma.serial.findMany({ + orderBy: { createdAt: "desc" }, + take: 10, + }), + ]); - const recentCompanies = await this.dbService.all<{ company_name: string; last_created: string; is_active: number }>(` - SELECT c.company_name, c.created_at as last_created, c.is_active - FROM companies c - ORDER BY c.updated_at DESC - LIMIT 10 - `); + const companyCount = companies.length; + const serialCount = serials.length; + const activeCount = serials.filter( + (s) => s.isActive && (!s.validUntil || s.validUntil > now), + ).length; + const inactiveCount = serialCount - activeCount; - const recentSerials = await this.dbService.all<{ serial_number: string; company_name: string; is_active: number; created_at: string }>(` - SELECT s.serial_number, s.company_name, s.is_active, s.created_at - FROM serials s - ORDER BY s.created_at DESC - LIMIT 10 - `); + const monthlyStats: Array<{ + month: string; + company_count: number; + serial_count: number; + }> = []; + const nowYear = now.getFullYear(); + const nowMonth = now.getMonth(); - let finalMonthlyStats = monthlyStats; - if (monthlyStats.length === 0) { - finalMonthlyStats = []; - const now = new Date(); - for (let i = 11; i >= 0; i--) { - const date = new Date(now.getFullYear(), now.getMonth() - i, 1); - const month = date.toISOString().substr(0, 7); - finalMonthlyStats.push({ - month, - company_count: 0, - serial_count: 0 + for (let i = 11; i >= 0; i--) { + const date = new Date(nowYear, nowMonth - i, 1); + const monthStr = date.toISOString().slice(0, 7); + const monthSerials = serials.filter((s) => { + const createdAt = new Date(s.createdAt); + return ( + createdAt.getFullYear() === date.getFullYear() && + createdAt.getMonth() === date.getMonth() + ); + }); + const uniqueCompanies = new Set(monthSerials.map((s) => s.companyName)); + + if (monthSerials.length > 0) { + monthlyStats.push({ + month: monthStr, + company_count: uniqueCompanies.size, + serial_count: monthSerials.length, }); } } return { - message: '获取统计数据成功', + message: "获取统计数据成功", data: { overview: { - totalCompanies: companyCount?.count || 0, - totalSerials: serialCount?.count || 0, - activeSerials: activeCount?.count || 0, - inactiveSerials: (serialCount?.count || 0) - (activeCount?.count || 0) + totalCompanies: companyCount, + totalSerials: serialCount, + activeSerials: activeCount, + inactiveSerials: inactiveCount, }, - monthlyStats: finalMonthlyStats.map(stat => ({ - month: stat.month, - company_count: stat.company_count, - serial_count: stat.serial_count + monthlyStats, + recentCompanies: recentCompanies.map((c) => ({ + companyName: c.companyName, + lastCreated: c.updatedAt, + status: c.isActive ? "active" : "disabled", })), - recentCompanies: recentCompanies.map(c => ({ - companyName: c.company_name, - lastCreated: c.last_created, - status: c.is_active ? 'active' : 'disabled' + recentSerials: recentSerials.map((s) => ({ + serialNumber: s.serialNumber, + companyName: s.companyName, + isActive: s.isActive, + createdAt: s.createdAt, })), - recentSerials: recentSerials.map(s => ({ - serialNumber: s.serial_number, - companyName: s.company_name, - isActive: !!s.is_active, - createdAt: s.created_at - })) - } + }, }; } } diff --git a/src/database/database.service.ts b/src/database/database.service.ts index 0d44a06..6249287 100644 --- a/src/database/database.service.ts +++ b/src/database/database.service.ts @@ -1,59 +1,25 @@ -import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import Database from 'better-sqlite3'; -import * as path from 'path'; -import * as fs from 'fs'; +import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; +import { PrismaClient } from "@prisma/client"; @Injectable() export class DatabaseService implements OnModuleInit, OnModuleDestroy { - private db: Database.Database; - private dbPath: string; + private prisma: PrismaClient; - constructor(private configService: ConfigService) {} + constructor() { + this.prisma = new PrismaClient({ + log: ["query", "error", "warn"], + }); + } onModuleInit() { - this.dbPath = this.configService.get('DB_PATH', path.join(process.cwd(), 'data/database.sqlite')); - const dbDir = path.dirname(this.dbPath); - - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - } - - this.db = new Database(this.dbPath, { verbose: console.log }); + this.prisma.$connect(); } onModuleDestroy() { - this.db.close(); + this.prisma.$disconnect(); } - get(sql: string, params: any[] = []): T | undefined { - try { - const stmt = this.db.prepare(sql); - return stmt.get(params) as T | undefined; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - all(sql: string, params: any[] = []): T[] { - try { - const stmt = this.db.prepare(sql); - return stmt.all(params) as T[]; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - run(sql: string, params: any[] = []): { id: number; changes: number } { - try { - const stmt = this.db.prepare(sql); - const result = stmt.run(params); - return { id: result.lastInsertRowid as number, changes: result.changes }; - } catch (error) { - console.error('数据库操作错误:', error); - throw error; - } + getPrisma() { + return this.prisma; } } diff --git a/src/database/database.ts b/src/database/database.ts deleted file mode 100644 index 91d4f41..0000000 --- a/src/database/database.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import fs from 'fs'; - -class DatabaseWrapper { - private db: Database.Database; - private dbPath: string; - - constructor() { - this.dbPath = process.env.DB_PATH || path.join(process.cwd(), 'data/database.sqlite'); - const dbDir = path.dirname(this.dbPath); - - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - } - - this.db = new Database(this.dbPath, { verbose: console.log }); - } - - get(sql: string, params: any[] = []): T | undefined { - try { - const stmt = this.db.prepare(sql); - return stmt.get(params) as T | undefined; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - all(sql: string, params: any[] = []): T[] { - try { - const stmt = this.db.prepare(sql); - return stmt.all(params) as T[]; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - run(sql: string, params: any[] = []): { id: number; changes: number } { - try { - const stmt = this.db.prepare(sql); - const result = stmt.run(params); - return { id: result.lastInsertRowid as number, changes: result.changes }; - } catch (error) { - console.error('数据库操作错误:', error); - throw error; - } - } - - close(): void { - this.db.close(); - } -} - -export default new DatabaseWrapper(); diff --git a/src/serials/serials.service.ts b/src/serials/serials.service.ts index bb9bf37..3cf669b 100644 --- a/src/serials/serials.service.ts +++ b/src/serials/serials.service.ts @@ -1,235 +1,281 @@ -import { Injectable } from '@nestjs/common'; -import { DatabaseService } from '../database/database.service'; -import * as QRCode from 'qrcode'; -import { Serial, SerialListItem } from './dto'; +import { Injectable } from "@nestjs/common"; +import { DatabaseService } from "../database/database.service"; +import * as QRCode from "qrcode"; +import { Serial, SerialListItem } from "./dto"; @Injectable() export class SerialsService { constructor(private dbService: DatabaseService) {} - async generate(companyName: string, quantity: number, validDays: number, userId: number, serialPrefix?: string): Promise { + async generate( + companyName: string, + quantity: number, + validDays: number, + userId: number, + serialPrefix?: string, + ): Promise { + const prisma = this.dbService.getPrisma(); const validUntil = new Date(); validUntil.setDate(validUntil.getDate() + validDays); - const existingCompany = await this.dbService.get('SELECT * FROM companies WHERE company_name = ?', [companyName]); + const existingCompany = await prisma.company.findUnique({ + where: { companyName }, + }); if (!existingCompany) { - await this.dbService.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]); + await prisma.company.create({ + data: { companyName, isActive: true }, + }); } const serials: SerialListItem[] = []; - const prefix = serialPrefix ? serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '') : 'BF' + new Date().getFullYear().toString().substr(2); + const prefix = serialPrefix + ? serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, "") + : "BF" + new Date().getFullYear().toString().substr(2); for (let i = 0; i < quantity; i++) { - const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0'); + const randomPart = Math.floor(Math.random() * 1000000) + .toString() + .padStart(6, "0"); const serialNumber = `${prefix}${randomPart}`; - - await this.dbService.run( - 'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)', - [serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), userId] - ); + + await prisma.serial.create({ + data: { + serialNumber, + companyName, + validUntil, + createdBy: userId, + isActive: true, + }, + }); serials.push({ serialNumber, companyName, validUntil: validUntil.toISOString(), isActive: true, - createdAt: new Date().toISOString() + createdAt: new Date().toISOString(), }); } return serials; } - async generateQRCode(serialNumber: string, baseUrl?: string, requestHost?: string, protocol?: string) { - const serial = await this.dbService.get<{ serial_number: string; company_name: string; is_active: number; valid_until: string | null }>( - 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', - [serialNumber.toUpperCase()] - ); + async generateQRCode( + serialNumber: string, + baseUrl?: string, + requestHost?: string, + protocol?: string, + ) { + const prisma = this.dbService.getPrisma(); + const serial = await prisma.serial.findUnique({ + where: { serialNumber: serialNumber.toUpperCase() }, + include: { + user: { + select: { name: true }, + }, + }, + }); if (!serial) { - throw new Error('序列号不存在'); + throw new Error("序列号不存在"); } - if (!serial.is_active) { - throw new Error('序列号已被禁用'); + if (!serial.isActive) { + throw new Error("序列号已被禁用"); } - if (serial.valid_until && new Date(serial.valid_until) < new Date()) { - throw new Error('序列号已过期'); + if (serial.validUntil && new Date(serial.validUntil) < new Date()) { + throw new Error("序列号已过期"); } if (!baseUrl) { baseUrl = `${protocol}://${requestHost}/query.html`; } - - const queryUrl = baseUrl.includes('?') - ? `${baseUrl}&serial=${serial.serial_number}` - : `${baseUrl}?serial=${serial.serial_number}`; + + const queryUrl = baseUrl.includes("?") + ? `${baseUrl}&serial=${serial.serialNumber}` + : `${baseUrl}?serial=${serial.serialNumber}`; const qrCodeData = await QRCode.toDataURL(queryUrl, { width: 200, color: { - dark: '#165DFF', - light: '#ffffff' - } + dark: "#165DFF", + light: "#ffffff", + }, }); return { - message: '二维码生成成功', + message: "二维码生成成功", qrCodeData, queryUrl, - serialNumber: serial.serial_number, - companyName: serial.company_name, - validUntil: serial.valid_until + serialNumber: serial.serialNumber, + companyName: serial.companyName, + validUntil: serial.validUntil, }; } async query(serialNumber: string) { - const serial = await this.dbService.get<{ serial_number: string; company_name: string; valid_until: string | null; is_active: number; created_at: string; created_by_name: string }>( - 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', - [serialNumber.toUpperCase()] - ); + const prisma = this.dbService.getPrisma(); + const serial = await prisma.serial.findUnique({ + where: { serialNumber: serialNumber.toUpperCase() }, + include: { + user: { + select: { name: true }, + }, + }, + }); if (!serial) { - throw new Error('序列号不存在'); + throw new Error("序列号不存在"); } - if (serial.valid_until && new Date(serial.valid_until) < new Date()) { - throw new Error('序列号已过期'); + if (serial.validUntil && new Date(serial.validUntil) < new Date()) { + throw new Error("序列号已过期"); } return { - message: '查询成功', + message: "查询成功", serial: { - serialNumber: serial.serial_number, - companyName: serial.company_name, - validUntil: serial.valid_until, - status: serial.is_active ? 'active' : 'disabled', - isActive: !!serial.is_active, - createdAt: serial.created_at, - createdBy: serial.created_by_name - } + serialNumber: serial.serialNumber, + companyName: serial.companyName, + validUntil: serial.validUntil, + status: serial.isActive ? "active" : "disabled", + isActive: serial.isActive, + createdAt: serial.createdAt, + createdBy: serial.user?.name, + }, }; } async findAll(page: number, limit: number, search: string) { + const prisma = this.dbService.getPrisma(); const offset = (page - 1) * limit; - let query = 'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id'; - let countQuery = 'SELECT COUNT(*) as total FROM serials s'; - let params: any[] = []; + const where = search + ? { + OR: [ + { serialNumber: { contains: search } }, + { companyName: { contains: search } }, + ], + } + : undefined; - if (search) { - query += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?'; - countQuery += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?'; - const searchParam = `%${search}%`; - params.push(searchParam, searchParam); - } - - query += ' ORDER BY s.created_at DESC LIMIT ? OFFSET ?'; - params.push(parseInt(limit.toString()), parseInt(offset.toString())); - - const [serials, countResult] = await Promise.all([ - this.dbService.all(query, params), - this.dbService.get<{ total: number }>(countQuery, params.slice(0, -2)) + const [serials, total] = await Promise.all([ + prisma.serial.findMany({ + where, + include: { + user: { + select: { name: true }, + }, + }, + orderBy: { createdAt: "desc" }, + skip: offset, + take: limit, + }), + prisma.serial.count({ where }), ]); - const total = countResult?.total || 0; const totalPages = Math.ceil(total / limit); return { - message: '获取序列号列表成功', - data: serials.map((s: any) => ({ - serialNumber: s.serial_number, - companyName: s.company_name, - validUntil: s.valid_until, - isActive: s.is_active, - createdAt: s.created_at, - createdBy: s.created_by_name + message: "获取序列号列表成功", + data: serials.map((s) => ({ + serialNumber: s.serialNumber, + companyName: s.companyName, + validUntil: s.validUntil, + isActive: s.isActive, + createdAt: s.createdAt, + createdBy: s.user?.name, })), pagination: { page: parseInt(page.toString()), limit: parseInt(limit.toString()), total, - totalPages - } + totalPages, + }, }; } - async update(serialNumber: string, updateData: { companyName?: string; validUntil?: string; isActive?: boolean }) { - const existingSerial = await this.dbService.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]); + async update( + serialNumber: string, + updateData: { + companyName?: string; + validUntil?: string; + isActive?: boolean; + }, + ) { + const prisma = this.dbService.getPrisma(); + const existingSerial = await prisma.serial.findUnique({ + where: { serialNumber: serialNumber.toUpperCase() }, + }); if (!existingSerial) { - throw new Error('序列号不存在'); + throw new Error("序列号不存在"); } - const updateFields: string[] = []; - const params: any[] = []; - + const updateFields: any = {}; if (updateData.companyName !== undefined) { - updateFields.push('company_name = ?'); - params.push(updateData.companyName); + updateFields.companyName = updateData.companyName; } - if (updateData.validUntil !== undefined) { - updateFields.push('valid_until = ?'); - params.push(updateData.validUntil); + updateFields.validUntil = new Date(updateData.validUntil); } - if (updateData.isActive !== undefined) { - updateFields.push('is_active = ?'); - params.push(updateData.isActive ? 1 : 0); + updateFields.isActive = updateData.isActive; } - if (updateFields.length === 0) { - throw new Error('没有提供更新字段'); + if (Object.keys(updateFields).length === 0) { + throw new Error("没有提供更新字段"); } - updateFields.push('updated_at = CURRENT_TIMESTAMP'); - params.push(serialNumber.toUpperCase()); - - await this.dbService.run( - `UPDATE serials SET ${updateFields.join(', ')} WHERE serial_number = ?`, - params - ); - - const updatedSerial = await this.dbService.get('SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?', [serialNumber.toUpperCase()]); + const updatedSerial = await prisma.serial.update({ + where: { serialNumber: serialNumber.toUpperCase() }, + data: updateFields, + include: { + user: { + select: { name: true }, + }, + }, + }); return { - message: '序列号更新成功', + message: "序列号更新成功", serial: { - serialNumber: (updatedSerial as any).serial_number, - companyName: (updatedSerial as any).company_name, - validUntil: (updatedSerial as any).valid_until, - isActive: (updatedSerial as any).is_active, - createdAt: (updatedSerial as any).created_at, - updatedAt: (updatedSerial as any).updated_at, - createdBy: (updatedSerial as any).created_by_name - } + serialNumber: updatedSerial.serialNumber, + companyName: updatedSerial.companyName, + validUntil: updatedSerial.validUntil, + isActive: updatedSerial.isActive, + createdAt: updatedSerial.createdAt, + updatedAt: updatedSerial.updatedAt, + createdBy: updatedSerial.user?.name, + }, }; } async revoke(serialNumber: string) { - const existingSerial = await this.dbService.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]); + const prisma = this.dbService.getPrisma(); + const existingSerial = await prisma.serial.findUnique({ + where: { serialNumber: serialNumber.toUpperCase() }, + }); if (!existingSerial) { - throw new Error('序列号不存在'); + throw new Error("序列号不存在"); } - if (!existingSerial.is_active) { - throw new Error('序列号已被吊销'); + if (!existingSerial.isActive) { + throw new Error("序列号已被吊销"); } - await this.dbService.run( - 'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?', - [serialNumber.toUpperCase()] - ); + await prisma.serial.update({ + where: { serialNumber: serialNumber.toUpperCase() }, + data: { isActive: false }, + }); return { - message: '序列号已吊销', + message: "序列号已吊销", data: { - serialNumber: serialNumber.toUpperCase() - } + serialNumber: serialNumber.toUpperCase(), + }, }; } } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 063616f..6030163 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -4,42 +4,42 @@ export interface User { password: string; name: string; email: string | null; - role: 'admin' | 'user'; - created_at: string; - updated_at: string; + role: "admin" | "user"; + createdAt: Date; + updatedAt: Date; } export interface Company { id: number; - company_name: string; - is_active: boolean; - created_at: string; - updated_at: string; + companyName: string; + isActive: boolean; + createdAt: Date; + updatedAt: Date; } export interface Serial { id: number; - serial_number: string; - company_name: string; - valid_until: string | null; - is_active: boolean; - created_by: number | null; - created_at: string; - updated_at: string; - created_by_name?: string; + serialNumber: string; + companyName: string; + validUntil: Date | null; + isActive: boolean; + createdBy: number | null; + createdAt: Date; + updatedAt: Date; + createdByName?: string; } export interface AuthUser { id: number; username: string; name: string; - role: 'admin' | 'user'; + role: "admin" | "user"; } export interface JWTPayload { userId: number; username: string; - role: 'admin' | 'user'; + role: "admin" | "user"; } export interface LoginRequest { @@ -105,7 +105,7 @@ export interface LoginResponse { username: string; name: string; email: string | null; - role: 'admin' | 'user'; + role: "admin" | "user"; }; } @@ -124,7 +124,7 @@ export interface CompanyListItem { lastCreated: string; serialCount: number; activeCount: number; - status: 'active' | 'disabled'; + status: "active" | "disabled"; } export interface CompanyDetail { @@ -135,7 +135,7 @@ export interface CompanyDetail { expiredCount: number; firstCreated: string; lastCreated: string; - status: 'active' | 'disabled'; + status: "active" | "disabled"; serials: SerialListItem[]; monthlyStats: MonthlyStat[]; } @@ -154,7 +154,20 @@ export interface StatsOverview { export interface StatsResponse { overview: StatsOverview; - monthlyStats: Array<{ month: string; company_count: number; serial_count: number }>; - recentCompanies: Array<{ companyName: string; lastCreated: string; status: 'active' | 'disabled' }>; - recentSerials: Array<{ serialNumber: string; companyName: string; isActive: boolean; createdAt: string }>; + monthlyStats: Array<{ + month: string; + company_count: number; + serial_count: number; + }>; + recentCompanies: Array<{ + companyName: string; + lastCreated: string; + status: "active" | "disabled"; + }>; + recentSerials: Array<{ + serialNumber: string; + companyName: string; + isActive: boolean; + createdAt: string; + }>; } diff --git a/src/utils/database.ts b/src/utils/database.ts deleted file mode 100644 index 91d4f41..0000000 --- a/src/utils/database.ts +++ /dev/null @@ -1,56 +0,0 @@ -import Database from 'better-sqlite3'; -import path from 'path'; -import fs from 'fs'; - -class DatabaseWrapper { - private db: Database.Database; - private dbPath: string; - - constructor() { - this.dbPath = process.env.DB_PATH || path.join(process.cwd(), 'data/database.sqlite'); - const dbDir = path.dirname(this.dbPath); - - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - } - - this.db = new Database(this.dbPath, { verbose: console.log }); - } - - get(sql: string, params: any[] = []): T | undefined { - try { - const stmt = this.db.prepare(sql); - return stmt.get(params) as T | undefined; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - all(sql: string, params: any[] = []): T[] { - try { - const stmt = this.db.prepare(sql); - return stmt.all(params) as T[]; - } catch (error) { - console.error('数据库查询错误:', error); - throw error; - } - } - - run(sql: string, params: any[] = []): { id: number; changes: number } { - try { - const stmt = this.db.prepare(sql); - const result = stmt.run(params); - return { id: result.lastInsertRowid as number, changes: result.changes }; - } catch (error) { - console.error('数据库操作错误:', error); - throw error; - } - } - - close(): void { - this.db.close(); - } -} - -export default new DatabaseWrapper();