Compare commits
7 Commits
239dec912c
...
64c3efd931
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64c3efd931 | ||
|
|
a949acbc3b | ||
|
|
0cbdb7f825 | ||
|
|
1308675f80 | ||
|
|
465057cce0 | ||
|
|
f3c775cea9 | ||
|
|
b83c59611c |
@ -29,3 +29,10 @@ REDIS_PORT="6379"
|
||||
REDIS_DB="0"
|
||||
REDIS_PASSWORD=""
|
||||
REDIS_KEY_PREFIX="sysmgr:"
|
||||
|
||||
# MinIO(对象存储,供 elder 模块上传评估结论副本)
|
||||
MINIO_ENDPOINT="localhost"
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY="minioadmin"
|
||||
MINIO_SECRET_KEY="minioadmin"
|
||||
MINIO_BUCKET="carelink"
|
||||
|
||||
@ -43,6 +43,7 @@
|
||||
"class-validator": "^0.15.1",
|
||||
"helmet": "^8.3.0",
|
||||
"ioredis": "^6.0.0",
|
||||
"minio": "^8.0.7",
|
||||
"nestjs-pino": "^5.3.1",
|
||||
"pino": "^10.4.0",
|
||||
"pino-pretty": "^13.2.0",
|
||||
|
||||
214
pnpm-lock.yaml
generated
214
pnpm-lock.yaml
generated
@ -205,6 +205,9 @@ importers:
|
||||
ioredis:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(supports-color@7.2.0)
|
||||
minio:
|
||||
specifier: ^8.0.7
|
||||
version: 8.0.7
|
||||
nestjs-pino:
|
||||
specifier: ^5.3.1
|
||||
version: 5.3.1(@nestjs/common@12.1.2(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@7.2.0))(@nestjs/core@12.1.2)(pino-http@11.0.0)(pino@10.4.0)(rxjs@7.8.2)
|
||||
@ -790,6 +793,9 @@ packages:
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@nodable/entities@3.1.0':
|
||||
resolution: {integrity: sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==}
|
||||
|
||||
'@oxc-project/types@0.153.0':
|
||||
resolution: {integrity: sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==}
|
||||
|
||||
@ -1224,6 +1230,9 @@ packages:
|
||||
resolution: {integrity: sha512-9k3v7xcHwgdO/DruxGIg4HtjvlAZlcnsX/mzqUb1t3NkYnl9kK2UJ+Gq0io+vQf7iT//BD/HB/NBkUR1LWxoeA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
anynum@1.0.1:
|
||||
resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==}
|
||||
|
||||
append-field@1.0.0:
|
||||
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
||||
|
||||
@ -1243,6 +1252,9 @@ packages:
|
||||
ast-v8-to-istanbul@1.0.7:
|
||||
resolution: {integrity: sha512-kFL68AG6ajd8fg248zwM9GQrUWEp79gsmjum34OEXjs4yHuUMZfYKwOLW9GMmB4oNvVrj+EAGxsP7ye2UR9UlA==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@ -1258,6 +1270,9 @@ packages:
|
||||
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
||||
hasBin: true
|
||||
|
||||
block-stream2@2.1.0:
|
||||
resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==}
|
||||
|
||||
body-parser@2.3.0:
|
||||
resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1266,6 +1281,13 @@ packages:
|
||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
browser-or-node@2.1.1:
|
||||
resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==}
|
||||
|
||||
buffer-crc32@1.0.0:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
@ -1428,6 +1450,10 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decode-uri-component@0.2.2:
|
||||
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
deepmerge-ts@7.1.5:
|
||||
resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
@ -1538,6 +1564,9 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
expect-type@1.4.0:
|
||||
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -1574,6 +1603,13 @@ packages:
|
||||
fast-wrap-ansi@0.2.2:
|
||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||
|
||||
fast-xml-builder@1.3.1:
|
||||
resolution: {integrity: sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==}
|
||||
|
||||
fast-xml-parser@5.11.2:
|
||||
resolution: {integrity: sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==}
|
||||
hasBin: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -1587,6 +1623,10 @@ packages:
|
||||
resolution: {integrity: sha512-sEB0oStmeLGtpmSAD88RDNuLsvhv4iC3u0lE25CUvfCULvj49xoQ3l3K525Vn+ZzsZ6DOLajQCV6RmhwwhqiJg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
filter-obj@1.1.0:
|
||||
resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
finalhandler@2.1.1:
|
||||
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
@ -1690,6 +1730,10 @@ packages:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
ipaddr.js@2.5.0:
|
||||
resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1705,6 +1749,9 @@ packages:
|
||||
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-unsafe@2.0.2:
|
||||
resolution: {integrity: sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==}
|
||||
|
||||
istanbul-lib-coverage@3.2.2:
|
||||
resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -1859,6 +1906,9 @@ packages:
|
||||
lodash.once@4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
log-symbols@7.0.1:
|
||||
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1932,6 +1982,10 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minio@8.0.7:
|
||||
resolution: {integrity: sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==}
|
||||
engines: {node: ^16 || ^18 || >=20}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@ -2029,6 +2083,10 @@ packages:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-expression-matcher@1.6.2:
|
||||
resolution: {integrity: sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
path-to-regexp@8.4.2:
|
||||
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
|
||||
|
||||
@ -2108,6 +2166,10 @@ packages:
|
||||
resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
query-string@7.1.3:
|
||||
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
@ -2122,6 +2184,10 @@ packages:
|
||||
rc9@2.1.2:
|
||||
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
@ -2170,6 +2236,10 @@ packages:
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
sax@1.6.1:
|
||||
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
secure-json-parse@4.1.0:
|
||||
resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==}
|
||||
|
||||
@ -2237,6 +2307,10 @@ packages:
|
||||
resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
split-on-first@1.1.0:
|
||||
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
split2@4.2.0:
|
||||
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
|
||||
engines: {node: '>= 10.x'}
|
||||
@ -2258,10 +2332,20 @@ packages:
|
||||
resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
stream-chain@2.2.5:
|
||||
resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==}
|
||||
|
||||
stream-json@1.9.1:
|
||||
resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
strict-uri-encode@2.0.0:
|
||||
resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2270,6 +2354,9 @@ packages:
|
||||
resolution: {integrity: sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
@ -2286,6 +2373,9 @@ packages:
|
||||
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
strnum@2.5.1:
|
||||
resolution: {integrity: sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==}
|
||||
|
||||
strtok3@10.3.5:
|
||||
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -2309,6 +2399,9 @@ packages:
|
||||
resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
through2@4.0.2:
|
||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@ -2386,6 +2479,9 @@ packages:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
validator@13.15.35:
|
||||
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@ -2494,6 +2590,18 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
xml-naming@0.3.0:
|
||||
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
||||
xml2js@0.6.2:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xmlbuilder@11.0.1:
|
||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
yaml@2.9.1:
|
||||
resolution: {integrity: sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==}
|
||||
engines: {node: '>= 14.6'}
|
||||
@ -2895,6 +3003,8 @@ snapshots:
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@nodable/entities@3.1.0': {}
|
||||
|
||||
'@oxc-project/types@0.153.0': {}
|
||||
|
||||
'@oxlint-tsgolint/darwin-arm64@7.0.2003':
|
||||
@ -3233,6 +3343,8 @@ snapshots:
|
||||
|
||||
ansis@4.4.0: {}
|
||||
|
||||
anynum@1.0.1: {}
|
||||
|
||||
append-field@1.0.0: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
@ -3249,6 +3361,8 @@ snapshots:
|
||||
estree-walker: 3.0.3
|
||||
js-tokens: 10.0.0
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
@ -3257,6 +3371,10 @@ snapshots:
|
||||
|
||||
bcryptjs@3.0.3: {}
|
||||
|
||||
block-stream2@2.1.0:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
body-parser@2.3.0(supports-color@7.2.0):
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -3275,6 +3393,10 @@ snapshots:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
|
||||
browser-or-node@2.1.1: {}
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
@ -3411,6 +3533,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
decode-uri-component@0.2.2: {}
|
||||
|
||||
deepmerge-ts@7.1.5: {}
|
||||
|
||||
defu@6.1.7: {}
|
||||
@ -3521,6 +3645,8 @@ snapshots:
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
expect-type@1.4.0: {}
|
||||
|
||||
express@5.2.1(supports-color@7.2.0):
|
||||
@ -3580,6 +3706,20 @@ snapshots:
|
||||
dependencies:
|
||||
fast-string-width: 3.0.2
|
||||
|
||||
fast-xml-builder@1.3.1:
|
||||
dependencies:
|
||||
path-expression-matcher: 1.6.2
|
||||
xml-naming: 0.3.0
|
||||
|
||||
fast-xml-parser@5.11.2:
|
||||
dependencies:
|
||||
'@nodable/entities': 3.1.0
|
||||
fast-xml-builder: 1.3.1
|
||||
is-unsafe: 2.0.2
|
||||
path-expression-matcher: 1.6.2
|
||||
strnum: 2.5.1
|
||||
xml-naming: 0.3.0
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.7):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.7
|
||||
@ -3593,6 +3733,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
filter-obj@1.1.0: {}
|
||||
|
||||
finalhandler@2.1.1(supports-color@7.2.0):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
@ -3709,6 +3851,8 @@ snapshots:
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
ipaddr.js@2.5.0: {}
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-interactive@2.0.0: {}
|
||||
@ -3717,6 +3861,8 @@ snapshots:
|
||||
|
||||
is-unicode-supported@2.1.0: {}
|
||||
|
||||
is-unsafe@2.0.2: {}
|
||||
|
||||
istanbul-lib-coverage@3.2.2: {}
|
||||
|
||||
istanbul-lib-report@3.0.1:
|
||||
@ -3839,6 +3985,8 @@ snapshots:
|
||||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
log-symbols@7.0.1:
|
||||
dependencies:
|
||||
is-unicode-supported: 2.1.0
|
||||
@ -3896,6 +4044,22 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minio@8.0.7:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
block-stream2: 2.1.0
|
||||
browser-or-node: 2.1.1
|
||||
buffer-crc32: 1.0.0
|
||||
eventemitter3: 5.0.4
|
||||
fast-xml-parser: 5.11.2
|
||||
ipaddr.js: 2.5.0
|
||||
lodash: 4.18.1
|
||||
mime-types: 2.1.35
|
||||
query-string: 7.1.3
|
||||
stream-json: 1.9.1
|
||||
through2: 4.0.2
|
||||
xml2js: 0.6.2
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
multer@2.4.0:
|
||||
@ -4002,6 +4166,8 @@ snapshots:
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-expression-matcher@1.6.2: {}
|
||||
|
||||
path-to-regexp@8.4.2: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
@ -4104,6 +4270,13 @@ snapshots:
|
||||
es-define-property: 1.0.1
|
||||
side-channel: 1.1.1
|
||||
|
||||
query-string@7.1.3:
|
||||
dependencies:
|
||||
decode-uri-component: 0.2.2
|
||||
filter-obj: 1.1.0
|
||||
split-on-first: 1.1.0
|
||||
strict-uri-encode: 2.0.0
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
range-parser@1.3.0: {}
|
||||
@ -4120,6 +4293,12 @@ snapshots:
|
||||
defu: 6.1.7
|
||||
destr: 2.0.5
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
readdirp@5.1.1: {}
|
||||
@ -4178,6 +4357,8 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sax@1.6.1: {}
|
||||
|
||||
secure-json-parse@4.1.0: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
@ -4264,6 +4445,8 @@ snapshots:
|
||||
|
||||
source-map@0.8.0: {}
|
||||
|
||||
split-on-first@1.1.0: {}
|
||||
|
||||
split2@4.2.0: {}
|
||||
|
||||
stackback@0.0.2: {}
|
||||
@ -4276,8 +4459,16 @@ snapshots:
|
||||
|
||||
stdin-discarder@0.3.2: {}
|
||||
|
||||
stream-chain@2.2.5: {}
|
||||
|
||||
stream-json@1.9.1:
|
||||
dependencies:
|
||||
stream-chain: 2.2.5
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
strict-uri-encode@2.0.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
@ -4289,6 +4480,10 @@ snapshots:
|
||||
get-east-asian-width: 1.7.0
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
@ -4301,6 +4496,10 @@ snapshots:
|
||||
|
||||
strip-json-comments@5.0.3: {}
|
||||
|
||||
strnum@2.5.1:
|
||||
dependencies:
|
||||
anynum: 1.0.1
|
||||
|
||||
strtok3@10.3.5:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
@ -4339,6 +4538,10 @@ snapshots:
|
||||
dependencies:
|
||||
real-require: 1.0.0
|
||||
|
||||
through2@4.0.2:
|
||||
dependencies:
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@1.3.1: {}
|
||||
@ -4401,6 +4604,8 @@ snapshots:
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
validator@13.15.35: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
@ -4466,6 +4671,15 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
xml-naming@0.3.0: {}
|
||||
|
||||
xml2js@0.6.2:
|
||||
dependencies:
|
||||
sax: 1.6.1
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xmlbuilder@11.0.1: {}
|
||||
|
||||
yaml@2.9.1:
|
||||
optional: true
|
||||
|
||||
|
||||
80
prisma/migrations/20261014084652_c1_elder/migration.sql
Normal file
80
prisma/migrations/20261014084652_c1_elder/migration.sql
Normal file
@ -0,0 +1,80 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `ltc_elder` (
|
||||
`id` VARCHAR(36) NOT NULL,
|
||||
`tenant_code` VARCHAR(64) NOT NULL,
|
||||
`loc_code` VARCHAR(12) NOT NULL,
|
||||
`prev_loc_code` VARCHAR(12) NULL,
|
||||
`transfer_confirm_at` DATETIME(3) NULL,
|
||||
`name` VARCHAR(30) NOT NULL,
|
||||
`gender` CHAR(1) NOT NULL,
|
||||
`birth_date` DATE NOT NULL,
|
||||
`id_card_no` VARCHAR(255) NOT NULL,
|
||||
`phone` VARCHAR(15) NULL,
|
||||
`address` VARCHAR(200) NOT NULL,
|
||||
`guardian_name` VARCHAR(30) NULL,
|
||||
`guardian_phone` VARCHAR(15) NULL,
|
||||
`insur_no` VARCHAR(30) NOT NULL,
|
||||
`insur_type` VARCHAR(10) NOT NULL,
|
||||
`insur_years` INTEGER NULL,
|
||||
`insur_verify_status` VARCHAR(20) NULL,
|
||||
`insur_verify_date` DATE NULL,
|
||||
`assess_level` VARCHAR(20) NULL,
|
||||
`care_level` VARCHAR(20) NULL,
|
||||
`assess_date` DATE NULL,
|
||||
`assess_org` VARCHAR(100) NULL,
|
||||
`assess_evidence_file` VARCHAR(255) NULL,
|
||||
`benefit_start` DATE NULL,
|
||||
`benefit_end` DATE NULL,
|
||||
`treat_status` VARCHAR(20) NOT NULL DEFAULT 'pending_file',
|
||||
`status_source` VARCHAR(10) NOT NULL DEFAULT 'manual',
|
||||
`status_change_date` DATE NULL,
|
||||
`hosp_start` DATE NULL,
|
||||
`hosp_end` DATE NULL,
|
||||
`photo_tier_override` JSON NULL,
|
||||
`need_assess` JSON NULL,
|
||||
`note` TEXT NULL,
|
||||
`biz_line` VARCHAR(20) NOT NULL DEFAULT 'ltci',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`created_by` VARCHAR(36) NULL,
|
||||
`updated_at` DATETIME(3) NOT NULL,
|
||||
`deleted_at` DATETIME(3) NULL,
|
||||
|
||||
INDEX `ltc_elder_tenant_code_name_idx`(`tenant_code`, `name`),
|
||||
UNIQUE INDEX `ltc_elder_tenant_code_id_card_no_key`(`tenant_code`, `id_card_no`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `ltc_elder_status_log` (
|
||||
`id` VARCHAR(36) NOT NULL,
|
||||
`elder_id` VARCHAR(36) NOT NULL,
|
||||
`tenant_code` VARCHAR(64) NOT NULL,
|
||||
`from_status` VARCHAR(20) NOT NULL,
|
||||
`to_status` VARCHAR(20) NOT NULL,
|
||||
`change_reason` VARCHAR(200) NOT NULL,
|
||||
`source` VARCHAR(10) NOT NULL DEFAULT 'manual',
|
||||
`operator_id` VARCHAR(36) NOT NULL,
|
||||
`evidence_file` VARCHAR(255) NULL,
|
||||
`changed_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
INDEX `ltc_elder_status_log_elder_id_idx`(`elder_id`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `ltc_staff` RENAME INDEX `sys_staff_tenant_code_name_idx` TO `ltc_staff_tenant_code_name_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `ltc_staff` RENAME INDEX `sys_staff_user_id_key` TO `ltc_staff_user_id_key`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `svc_local` RENAME INDEX `sys_svc_local_loc_code_idx` TO `svc_local_loc_code_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `svc_mapping` RENAME INDEX `sys_svc_mapping_loc_id_idx` TO `svc_mapping_loc_id_idx`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `svc_national` RENAME INDEX `sys_svc_national_nat_code_key` TO `svc_national_nat_code_key`;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER TABLE `svc_org` RENAME INDEX `sys_svc_org_tenant_code_src_type_idx` TO `svc_org_tenant_code_src_type_idx`;
|
||||
@ -425,3 +425,66 @@ model LocConfig {
|
||||
|
||||
@@map("loc_config")
|
||||
}
|
||||
|
||||
/// 老人档案
|
||||
model Elder {
|
||||
id String @id @default(uuid(7)) @db.VarChar(36)
|
||||
tenantCode String @map("tenant_code") @db.VarChar(64)
|
||||
locCode String @map("loc_code") @db.VarChar(12)
|
||||
prevLocCode String? @map("prev_loc_code") @db.VarChar(12)
|
||||
transferConfirmAt DateTime? @map("transfer_confirm_at")
|
||||
name String @db.VarChar(30)
|
||||
gender String @db.Char(1)
|
||||
birthDate DateTime @map("birth_date") @db.Date
|
||||
idCardNo String @map("id_card_no") @db.VarChar(255)
|
||||
phone String? @db.VarChar(15)
|
||||
address String @db.VarChar(200)
|
||||
guardianName String? @map("guardian_name") @db.VarChar(30)
|
||||
guardianPhone String? @map("guardian_phone") @db.VarChar(15)
|
||||
insurNo String @map("insur_no") @db.VarChar(30)
|
||||
insurType String @map("insur_type") @db.VarChar(10)
|
||||
insurYears Int? @map("insur_years")
|
||||
insurVerifyStatus String? @map("insur_verify_status") @db.VarChar(20)
|
||||
insurVerifyDate DateTime? @map("insur_verify_date") @db.Date
|
||||
assessLevel String? @map("assess_level") @db.VarChar(20)
|
||||
careLevel String? @map("care_level") @db.VarChar(20)
|
||||
assessDate DateTime? @map("assess_date") @db.Date
|
||||
assessOrg String? @map("assess_org") @db.VarChar(100)
|
||||
assessEvidenceFile String? @map("assess_evidence_file") @db.VarChar(255)
|
||||
benefitStart DateTime? @map("benefit_start") @db.Date
|
||||
benefitEnd DateTime? @map("benefit_end") @db.Date
|
||||
treatStatus String @default("pending_file") @map("treat_status") @db.VarChar(20)
|
||||
statusSource String @default("manual") @map("status_source") @db.VarChar(10)
|
||||
statusChangeDate DateTime? @map("status_change_date") @db.Date
|
||||
hospStart DateTime? @map("hosp_start") @db.Date
|
||||
hospEnd DateTime? @map("hosp_end") @db.Date
|
||||
photoTierOverride Json? @map("photo_tier_override")
|
||||
needAssess Json? @map("need_assess")
|
||||
note String? @db.Text
|
||||
bizLine String @default("ltci") @map("biz_line") @db.VarChar(20)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
createdBy String? @map("created_by") @db.VarChar(36)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
@@unique([tenantCode, idCardNo])
|
||||
@@index([tenantCode, name])
|
||||
@@map("ltc_elder")
|
||||
}
|
||||
|
||||
/// 待遇状态变更日志
|
||||
model ElderStatusLog {
|
||||
id String @id @default(uuid(7)) @db.VarChar(36)
|
||||
elderId String @map("elder_id") @db.VarChar(36)
|
||||
tenantCode String @map("tenant_code") @db.VarChar(64)
|
||||
fromStatus String @map("from_status") @db.VarChar(20)
|
||||
toStatus String @map("to_status") @db.VarChar(20)
|
||||
changeReason String @map("change_reason") @db.VarChar(200)
|
||||
source String @default("manual") @db.VarChar(10)
|
||||
operatorId String @map("operator_id") @db.VarChar(36)
|
||||
evidenceFile String? @map("evidence_file") @db.VarChar(255)
|
||||
changedAt DateTime @default(now()) @map("changed_at")
|
||||
|
||||
@@index([elderId])
|
||||
@@map("ltc_elder_status_log")
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ import { NoticesModule } from './notices/notices.module.js';
|
||||
import { AgencyModule } from './agency/agency.module.js';
|
||||
import { StaffModule } from './staff/staff.module.js';
|
||||
import { CatalogModule } from './catalog/catalog.module.js';
|
||||
import { StorageModule } from './storage/storage.module.js';
|
||||
import { ElderModule } from './elder/elder.module.js';
|
||||
import { AppController } from './app.controller.js';
|
||||
import { AppService } from './app.service.js';
|
||||
|
||||
@ -53,6 +55,8 @@ import { AppService } from './app.service.js';
|
||||
AgencyModule,
|
||||
StaffModule,
|
||||
CatalogModule,
|
||||
StorageModule,
|
||||
ElderModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@ -39,6 +39,11 @@ export const ErrorCodes = {
|
||||
SVC_LOC_NOT_FOUND: 42001,
|
||||
SVC_ORG_NOT_FOUND: 42002,
|
||||
SVC_LOC_IMMUTABLE: 42003,
|
||||
// 服务机构端·老人档案(43)
|
||||
ELDER_NOT_FOUND: 43001,
|
||||
ELDER_ID_CARD_EXISTS: 43002,
|
||||
ELDER_STATUS_REASON_REQUIRED: 43003,
|
||||
ELDER_LEVEL_REQUIRES_EVIDENCE: 43004,
|
||||
} as const;
|
||||
|
||||
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
|
||||
|
||||
@ -91,6 +91,13 @@ export const APIS: { code: string; name: string; method: string; path: string; p
|
||||
{ code: Permissions.svcOrg.list, name: '机构项目列表', method: 'GET', path: '/api/v1/svc/org/list', parent: 'svc-config-catalog' },
|
||||
{ code: Permissions.svcOrg.config, name: '机构项目配置', method: 'POST', path: '/api/v1/svc/org/config', parent: 'svc-config-catalog' },
|
||||
{ code: Permissions.svcOrg.newNational, name: '国家新增项', method: 'GET', path: '/api/v1/svc/org/new-national', parent: 'svc-config-loc' },
|
||||
// 服务机构端·老人档案(C1)
|
||||
{ code: Permissions.elder.list, name: '老人列表', method: 'GET', path: '/api/v1/elder', parent: 'elder' },
|
||||
{ code: Permissions.elder.create, name: '创建老人', method: 'POST', path: '/api/v1/elder', parent: 'elder' },
|
||||
{ code: Permissions.elder.detail, name: '老人详情', method: 'GET', path: '/api/v1/elder/:id', parent: 'elder' },
|
||||
{ code: Permissions.elder.statusChange, name: '变更待遇状态', method: 'POST', path: '/api/v1/elder/:id/status', parent: 'elder' },
|
||||
{ code: Permissions.elder.detail, name: '状态日志', method: 'GET', path: '/api/v1/elder/:id/status/log', parent: 'elder' },
|
||||
{ code: Permissions.elder.create, name: '上传评估结论', method: 'POST', path: '/api/v1/elder/assess-evidence', parent: 'elder' },
|
||||
];
|
||||
|
||||
/** 按钮资源(纯前端按钮,无独立受控接口)。 */
|
||||
|
||||
182
src/elder/dto.ts
Normal file
182
src/elder/dto.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsDateString, IsIn, IsInt, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
|
||||
import { PageQuery } from '../common/page.dto.js';
|
||||
|
||||
/** 十态待遇状态机(唯一来源,见 docs/superpowers/specs C1 设计 §4)。 */
|
||||
export const TREAT_STATUSES = [
|
||||
'pending_file',
|
||||
'waiting_period',
|
||||
'active',
|
||||
'hosp_pause',
|
||||
'review_waiting',
|
||||
'transferring',
|
||||
'dead',
|
||||
'long_away',
|
||||
'benefit_expired',
|
||||
'terminated',
|
||||
] as const;
|
||||
|
||||
/** 不可逆态:处于这些状态时禁止再变更。 */
|
||||
export const IRREVERSIBLE_TREAT_STATUSES: readonly string[] = ['dead', 'terminated'];
|
||||
|
||||
/**
|
||||
* 老人档案(elder)DTO。字段字典以 shared/data-model 为准(ltc_elder)。
|
||||
* - 不含 tenantCode/createdBy/createdAt/updatedAt/deletedAt(系统字段,服务端注入);
|
||||
* - birth_date 由身份证 7-14 位带出,不接受前端传入;
|
||||
* - treat_status/status_source 由服务端定,不接受前端传入;
|
||||
* - id_card_no 本期存明文(加密机制由后续安全 change 统一落地,见 elder.service.ts TODO)。
|
||||
*/
|
||||
export class CreateElderDto {
|
||||
@ApiProperty({ description: '参保地/统筹区 code(loc_code)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
locCode!: string;
|
||||
|
||||
@ApiProperty({ description: '姓名' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(30)
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ description: '性别:M/F' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(['M', 'F'])
|
||||
gender!: string;
|
||||
|
||||
@ApiProperty({ description: '身份证号(18 位,服务端做 GB11643 校验位校验)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
idCardNo!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '联系电话' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ description: '居住地址' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(200)
|
||||
address!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '紧急联系人' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
guardianName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '紧急联系人电话' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
guardianPhone?: string;
|
||||
|
||||
@ApiProperty({ description: '医保参保号' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
insurNo!: string;
|
||||
|
||||
@ApiProperty({ description: '参保类型:employee/resident' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
insurType!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '缴费年限' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
insurYears?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '参保核验状态:verified/unverified' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
insurVerifyStatus?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '参保核验日期(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
insurVerifyDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '失能等级(sev_1/sev_2/sev_3),有值则须上传评估结论副本' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assessLevel?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '护理等级(care_1/care_2/care_3),有值则须上传评估结论副本' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
careLevel?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '评估结论日期(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
assessDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '评估机构名称' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assessOrg?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '评估结论副本 MinIO key(有等级则必填)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
assessEvidenceFile?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '待遇有效期起(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
benefitStart?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '待遇有效期止(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
benefitEnd?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '住院起(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
hospStart?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '住院止(YYYY-MM-DD)' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
hospEnd?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '备注' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export class ListElderQuery extends PageQuery {
|
||||
@ApiPropertyOptional({ description: '姓名(模糊)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '待遇状态' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
treatStatus?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '失能/护理等级(匹配 assess_level 或 care_level)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export class ChangeStatusDto {
|
||||
@ApiProperty({ description: '目标待遇状态(十态之一)', enum: TREAT_STATUSES })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@IsIn(TREAT_STATUSES)
|
||||
toStatus!: string;
|
||||
|
||||
@ApiProperty({ description: '变更原因(必填)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
changeReason!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '佐证材料 MinIO key' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
evidenceFile?: string;
|
||||
}
|
||||
51
src/elder/elder.controller.ts
Normal file
51
src/elder/elder.controller.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { Permissions } from '../common/permissions.js';
|
||||
import { AuditLog, RequirePermissions } from '../auth/decorators.js';
|
||||
import { ChangeStatusDto, CreateElderDto, ListElderQuery } from './dto.js';
|
||||
import { ElderService } from './elder.service.js';
|
||||
|
||||
@Controller('elder')
|
||||
export class ElderController {
|
||||
constructor(private readonly service: ElderService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions(Permissions.elder.list)
|
||||
list(@Query() q: ListElderQuery) {
|
||||
return this.service.listElder(q);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions(Permissions.elder.create)
|
||||
@AuditLog('elder', 'create')
|
||||
create(@Body() dto: CreateElderDto) {
|
||||
return this.service.createElder(dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions(Permissions.elder.detail)
|
||||
detail(@Param('id') id: string) {
|
||||
return this.service.detail(id);
|
||||
}
|
||||
|
||||
@Post(':id/status')
|
||||
@RequirePermissions(Permissions.elder.statusChange)
|
||||
@AuditLog('elder', 'status-change')
|
||||
changeStatus(@Param('id') id: string, @Body() dto: ChangeStatusDto) {
|
||||
return this.service.changeStatus(id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/status/log')
|
||||
@RequirePermissions(Permissions.elder.detail)
|
||||
statusLog(@Param('id') id: string) {
|
||||
return this.service.statusLog(id);
|
||||
}
|
||||
|
||||
@Post('assess-evidence')
|
||||
@RequirePermissions(Permissions.elder.create)
|
||||
@AuditLog('elder', 'upload-evidence')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
uploadEvidence(@UploadedFile() file: { buffer: Buffer; originalname: string }) {
|
||||
return this.service.uploadEvidence(file);
|
||||
}
|
||||
}
|
||||
12
src/elder/elder.module.ts
Normal file
12
src/elder/elder.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ElderController } from './elder.controller.js';
|
||||
import { ElderService } from './elder.service.js';
|
||||
import { StorageModule } from '../storage/storage.module.js';
|
||||
import { AuditLogsModule } from '../audit-logs/audit-logs.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [StorageModule, AuditLogsModule],
|
||||
controllers: [ElderController],
|
||||
providers: [ElderService],
|
||||
})
|
||||
export class ElderModule {}
|
||||
315
src/elder/elder.service.spec.ts
Normal file
315
src/elder/elder.service.spec.ts
Normal file
@ -0,0 +1,315 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ElderService, maskIdCard, toDateOnly } from './elder.service.js';
|
||||
import type { PrismaService } from '../prisma/prisma.service.js';
|
||||
import type { StorageService } from '../storage/storage.service.js';
|
||||
import type { AuditLogsService } from '../audit-logs/audit-logs.service.js';
|
||||
import { ErrorCodes } from '../common/error-codes.js';
|
||||
import { runWithRequestContext } from '../common/context/request-context.js';
|
||||
import type { RequestContext } from '../common/context/request-context.js';
|
||||
|
||||
const ctx: RequestContext = {
|
||||
tenantCode: 'T001',
|
||||
userId: 'u1',
|
||||
username: 'admin',
|
||||
super: false,
|
||||
dataScope: 'all',
|
||||
orgCode: null,
|
||||
orgCodes: [],
|
||||
};
|
||||
|
||||
/** 通过 GB11643 校验位校验的合法身份证(出生日期 1990-01-01)。 */
|
||||
const VALID_ID_CARD = '530102199001011231';
|
||||
|
||||
const baseDto = {
|
||||
locCode: '530100',
|
||||
name: '张三',
|
||||
gender: 'M',
|
||||
idCardNo: VALID_ID_CARD,
|
||||
phone: '13800000000',
|
||||
address: '昆明市五华区',
|
||||
insurNo: 'INS001',
|
||||
insurType: 'employee',
|
||||
};
|
||||
|
||||
function makeService(
|
||||
overrides: {
|
||||
elderFindFirst?: ReturnType<typeof vi.fn>;
|
||||
elderFindUnique?: ReturnType<typeof vi.fn>;
|
||||
elderFindMany?: ReturnType<typeof vi.fn>;
|
||||
elderCount?: ReturnType<typeof vi.fn>;
|
||||
elderCreate?: ReturnType<typeof vi.fn>;
|
||||
elderUpdate?: ReturnType<typeof vi.fn>;
|
||||
userRoleFindMany?: ReturnType<typeof vi.fn>;
|
||||
statusLogCreate?: ReturnType<typeof vi.fn>;
|
||||
statusLogFindMany?: ReturnType<typeof vi.fn>;
|
||||
auditRecord?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
const elderFindFirst = overrides.elderFindFirst ?? vi.fn().mockResolvedValue(null);
|
||||
const elderFindUnique = overrides.elderFindUnique ?? vi.fn().mockResolvedValue(null);
|
||||
const elderFindMany = overrides.elderFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const elderCount = overrides.elderCount ?? vi.fn().mockResolvedValue(0);
|
||||
const elderCreate = overrides.elderCreate ?? vi.fn().mockResolvedValue({ id: 'e1', ...baseDto });
|
||||
const elderUpdate = overrides.elderUpdate ?? vi.fn().mockResolvedValue({ id: 'e1' });
|
||||
const userRoleFindMany = overrides.userRoleFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const statusLogCreate = overrides.statusLogCreate ?? vi.fn().mockResolvedValue({ id: 'sl1' });
|
||||
const statusLogFindMany = overrides.statusLogFindMany ?? vi.fn().mockResolvedValue([]);
|
||||
const auditRecord = overrides.auditRecord ?? vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
const prisma = {
|
||||
client: {
|
||||
elder: {
|
||||
findFirst: elderFindFirst,
|
||||
findUnique: elderFindUnique,
|
||||
findMany: elderFindMany,
|
||||
count: elderCount,
|
||||
create: elderCreate,
|
||||
update: elderUpdate,
|
||||
},
|
||||
elderStatusLog: { create: statusLogCreate, findMany: statusLogFindMany },
|
||||
userRole: { findMany: userRoleFindMany },
|
||||
},
|
||||
};
|
||||
const storage = {
|
||||
buildKey: vi.fn().mockReturnValue('elder/T001/1-a.jpg'),
|
||||
upload: vi.fn().mockResolvedValue('elder/T001/1-a.jpg'),
|
||||
};
|
||||
const auditLogs = { record: auditRecord };
|
||||
const service = new ElderService(
|
||||
prisma as unknown as PrismaService,
|
||||
storage as unknown as StorageService,
|
||||
auditLogs as unknown as AuditLogsService,
|
||||
);
|
||||
return {
|
||||
service,
|
||||
elderFindFirst,
|
||||
elderFindUnique,
|
||||
elderFindMany,
|
||||
elderCount,
|
||||
elderCreate,
|
||||
elderUpdate,
|
||||
userRoleFindMany,
|
||||
statusLogCreate,
|
||||
statusLogFindMany,
|
||||
auditRecord,
|
||||
storage,
|
||||
};
|
||||
}
|
||||
|
||||
describe('maskIdCard', () => {
|
||||
it('18 位 → 前4 + 10个* + 后4', () => {
|
||||
expect(maskIdCard('530102199001011231')).toBe('5301**********1231');
|
||||
});
|
||||
it('15 位 → 前3 + 8个* + 后4', () => {
|
||||
expect(maskIdCard('530102900101123')).toBe('530********1123');
|
||||
});
|
||||
it('空串与非常见长度原样返回', () => {
|
||||
expect(maskIdCard('')).toBe('');
|
||||
expect(maskIdCard('123')).toBe('123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toDateOnly', () => {
|
||||
it('date-only 字符串 → UTC 零点 Date(不提前一天)', () => {
|
||||
expect(toDateOnly('1990-01-01')?.toISOString()).toBe('1990-01-01T00:00:00.000Z');
|
||||
expect(toDateOnly('2025-01-01')?.toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('空值返回 undefined', () => {
|
||||
expect(toDateOnly(undefined)).toBeUndefined();
|
||||
expect(toDateOnly('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ElderService', () => {
|
||||
it('create 有等级无评估结论副本 → 抛 ELDER_LEVEL_REQUIRES_EVIDENCE', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () => service.createElder({ ...baseDto, assessLevel: 'sev_1' })),
|
||||
).rejects.toMatchObject({ response: { code: ErrorCodes.ELDER_LEVEL_REQUIRES_EVIDENCE } });
|
||||
|
||||
expect(elderCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('create benefit_start 未来 → treatStatus = waiting_period', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.createElder({ ...baseDto, benefitStart: '2099-01-01', benefitEnd: '2099-12-31' }),
|
||||
);
|
||||
|
||||
expect(elderCreate).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ treatStatus: 'waiting_period', statusSource: 'manual' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('create 待遇有效期覆盖今天 → treatStatus = active,且出生日期由身份证带出为 Date', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.createElder({ ...baseDto, benefitStart: '2000-01-01', benefitEnd: '2099-12-31' }),
|
||||
);
|
||||
|
||||
expect(elderCreate).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({ treatStatus: 'active', birthDate: expect.any(Date) }),
|
||||
});
|
||||
});
|
||||
|
||||
it('合法建档(含日期字段)成功创建,日期字段转为 UTC 零点 Date 且值正确', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await runWithRequestContext(ctx, () =>
|
||||
service.createElder({
|
||||
...baseDto,
|
||||
benefitStart: '2025-01-01',
|
||||
benefitEnd: '2025-12-31',
|
||||
assessDate: '2025-01-15',
|
||||
insurVerifyDate: '2025-01-10',
|
||||
hospStart: '2025-02-01',
|
||||
hospEnd: '2025-02-05',
|
||||
}),
|
||||
);
|
||||
|
||||
const data = elderCreate.mock.calls[0][0].data as Record<string, unknown>;
|
||||
expect(data.birthDate).toBeInstanceOf(Date);
|
||||
expect((data.birthDate as Date).toISOString()).toBe('1990-01-01T00:00:00.000Z');
|
||||
expect((data.benefitStart as Date).toISOString()).toBe('2025-01-01T00:00:00.000Z');
|
||||
expect((data.benefitEnd as Date).toISOString()).toBe('2025-12-31T00:00:00.000Z');
|
||||
expect((data.assessDate as Date).toISOString()).toBe('2025-01-15T00:00:00.000Z');
|
||||
expect((data.insurVerifyDate as Date).toISOString()).toBe('2025-01-10T00:00:00.000Z');
|
||||
expect((data.hospStart as Date).toISOString()).toBe('2025-02-01T00:00:00.000Z');
|
||||
expect((data.hospEnd as Date).toISOString()).toBe('2025-02-05T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('同机构重复 id_card_no → 抛 ELDER_ID_CARD_EXISTS', async () => {
|
||||
const elderFindFirst = vi.fn().mockResolvedValue({ id: 'e-other', idCardNo: VALID_ID_CARD });
|
||||
const elderCreate = vi.fn();
|
||||
const { service } = makeService({ elderFindFirst, elderCreate });
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () => service.createElder(baseDto)),
|
||||
).rejects.toMatchObject({ response: { code: ErrorCodes.ELDER_ID_CARD_EXISTS } });
|
||||
|
||||
expect(elderFindFirst).toHaveBeenCalledWith({ where: { idCardNo: VALID_ID_CARD } });
|
||||
expect(elderCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('changeStatus 缺 changeReason → 抛 ELDER_STATUS_REASON_REQUIRED', async () => {
|
||||
const { service, elderUpdate } = makeService();
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () => service.changeStatus('e1', { toStatus: 'active', changeReason: '' })),
|
||||
).rejects.toMatchObject({ response: { code: ErrorCodes.ELDER_STATUS_REASON_REQUIRED } });
|
||||
|
||||
expect(elderUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('changeStatus 写状态日志(from/to/reason/operator)', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'waiting_period', name: '张三' });
|
||||
const { service, statusLogCreate } = makeService({ elderFindUnique });
|
||||
|
||||
const result = await runWithRequestContext(ctx, () =>
|
||||
service.changeStatus('e1', { toStatus: 'active', changeReason: '待遇生效' }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({ id: 'e1', fromStatus: 'waiting_period', toStatus: 'active' });
|
||||
expect(statusLogCreate).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
elderId: 'e1',
|
||||
tenantCode: 'T001',
|
||||
fromStatus: 'waiting_period',
|
||||
toStatus: 'active',
|
||||
changeReason: '待遇生效',
|
||||
operatorId: 'u1',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('changeStatus 对不可逆态(dead/terminated)拒绝变更', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'dead', name: '张三' });
|
||||
const { service, statusLogCreate } = makeService({ elderFindUnique });
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () => service.changeStatus('e1', { toStatus: 'active', changeReason: '恢复' })),
|
||||
).rejects.toThrowError('当前待遇状态不可再变更');
|
||||
|
||||
expect(statusLogCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('changeStatus 后 statusLog 返回写入的日志', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', treatStatus: 'waiting_period', name: '张三' });
|
||||
const statusLogFindMany = vi.fn().mockResolvedValue([
|
||||
{ id: 'sl1', elderId: 'e1', fromStatus: 'waiting_period', toStatus: 'active', changeReason: '待遇生效', operatorId: 'u1' },
|
||||
]);
|
||||
const { service, statusLogCreate } = makeService({ elderFindUnique, statusLogFindMany });
|
||||
|
||||
await runWithRequestContext(ctx, () => service.changeStatus('e1', { toStatus: 'active', changeReason: '待遇生效' }));
|
||||
const logs = await runWithRequestContext(ctx, () => service.statusLog('e1'));
|
||||
|
||||
expect(statusLogCreate).toHaveBeenCalled();
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0]).toMatchObject({ fromStatus: 'waiting_period', toStatus: 'active', operatorId: 'u1' });
|
||||
});
|
||||
|
||||
it('create 待遇止早于起 → 抛 400', async () => {
|
||||
const { service, elderCreate } = makeService();
|
||||
|
||||
await expect(
|
||||
runWithRequestContext(ctx, () =>
|
||||
service.createElder({ ...baseDto, benefitStart: '2025-01-01', benefitEnd: '2024-01-01' }),
|
||||
),
|
||||
).rejects.toThrowError('待遇有效期止须晚于待遇有效期起');
|
||||
|
||||
expect(elderCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('list 返回掩码 id_card_no', async () => {
|
||||
const elderFindMany = vi.fn().mockResolvedValue([
|
||||
{ id: 'e1', idCardNo: VALID_ID_CARD, name: '张三', treatStatus: 'active' },
|
||||
]);
|
||||
const elderCount = vi.fn().mockResolvedValue(1);
|
||||
const { service } = makeService({ elderFindMany, elderCount });
|
||||
|
||||
const res = await runWithRequestContext(ctx, () => service.listElder({ page: 1, pageSize: 20 }));
|
||||
|
||||
expect(res.list[0].idCardNo).toBe('5301**********1231');
|
||||
expect(res.total).toBe(1);
|
||||
});
|
||||
|
||||
it('detail 非 admin/qc 不返回明文', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', idCardNo: VALID_ID_CARD, name: '张三' });
|
||||
const userRoleFindMany = vi.fn().mockResolvedValue([{ roleCode: 'nurse' }]);
|
||||
const { service, auditRecord } = makeService({ elderFindUnique, userRoleFindMany });
|
||||
|
||||
const res = await runWithRequestContext(ctx, () => service.detail('e1'));
|
||||
|
||||
expect(res.idCardNo).toBe('5301**********1231');
|
||||
expect(auditRecord).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('detail admin/qc 返回明文并写审计', async () => {
|
||||
const elderFindUnique = vi.fn().mockResolvedValue({ id: 'e1', idCardNo: VALID_ID_CARD, name: '张三' });
|
||||
const userRoleFindMany = vi.fn().mockResolvedValue([{ roleCode: 'admin' }]);
|
||||
const { service, auditRecord } = makeService({ elderFindUnique, userRoleFindMany });
|
||||
|
||||
const res = await runWithRequestContext(ctx, () => service.detail('e1'));
|
||||
|
||||
expect(res.idCardNo).toBe(VALID_ID_CARD);
|
||||
expect(auditRecord).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ module: 'elder', action: 'view-id-card', userId: 'u1' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('uploadEvidence 读 buffer → buildKey + upload → 返回 { key }', async () => {
|
||||
const { service, storage } = makeService();
|
||||
|
||||
const res = await runWithRequestContext(ctx, () =>
|
||||
service.uploadEvidence({ buffer: Buffer.from('x'), originalname: 'a.jpg' }),
|
||||
);
|
||||
|
||||
expect(storage.buildKey).toHaveBeenCalledWith('T001', 'a.jpg');
|
||||
expect(storage.upload).toHaveBeenCalledWith(expect.any(Buffer), 'elder/T001/1-a.jpg');
|
||||
expect(res).toEqual({ key: 'elder/T001/1-a.jpg' });
|
||||
});
|
||||
});
|
||||
226
src/elder/elder.service.ts
Normal file
226
src/elder/elder.service.ts
Normal file
@ -0,0 +1,226 @@
|
||||
import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { BaseCrudService } from '../common/base-crud.service.js';
|
||||
import { PrismaService } from '../prisma/prisma.service.js';
|
||||
import { getRequestContext } from '../common/context/request-context.js';
|
||||
import { BusinessException } from '../common/business-exception.js';
|
||||
import { ErrorCodes } from '../common/error-codes.js';
|
||||
import { StorageService } from '../storage/storage.service.js';
|
||||
import { AuditLogsService } from '../audit-logs/audit-logs.service.js';
|
||||
import { ChangeStatusDto, CreateElderDto, IRREVERSIBLE_TREAT_STATUSES, ListElderQuery } from './dto.js';
|
||||
|
||||
/**
|
||||
* 身份证掩码:18 位 → 前4 + 10个* + 后4;15 位 → 前3 + 8个* + 后4;其余原样。
|
||||
* 列表/非授权详情返回掩码;明文仅 admin/qc(或平台超管)可见且落审计。
|
||||
*/
|
||||
export function maskIdCard(id: string): string {
|
||||
if (!id) return '';
|
||||
if (id.length === 18) return `${id.slice(0, 4)}**********${id.slice(-4)}`;
|
||||
if (id.length === 15) return `${id.slice(0, 3)}********${id.slice(-4)}`;
|
||||
return id;
|
||||
}
|
||||
|
||||
/** GB11643-1999 加权因子与校验码映射。 */
|
||||
const ID_WEIGHTS = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
|
||||
const ID_CHECK_CODES = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
|
||||
|
||||
/** 身份证 18 位 + 校验位校验,通过返回出生日期(YYYY-MM-DD)。 */
|
||||
function deriveBirthDate(idCardNo: string): string {
|
||||
if (!/^\d{17}[\dXx]$/.test(idCardNo)) {
|
||||
throw new BadRequestException('身份证号格式不正确(须 18 位,末位可为 X)');
|
||||
}
|
||||
const sum = ID_WEIGHTS.reduce((acc, w, i) => acc + w * Number(idCardNo[i]), 0);
|
||||
if (ID_CHECK_CODES[sum % 11] !== idCardNo[17].toUpperCase()) {
|
||||
throw new BadRequestException('身份证号校验位错误');
|
||||
}
|
||||
return `${idCardNo.slice(6, 10)}-${idCardNo.slice(10, 12)}-${idCardNo.slice(12, 14)}`;
|
||||
}
|
||||
|
||||
/** 本地今天(YYYY-MM-DD,字符串比较即日期比较)。 */
|
||||
function todayStr(): string {
|
||||
const d = new Date();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${m}-${day}`;
|
||||
}
|
||||
|
||||
/** 建档初始状态:待遇有效期覆盖今天 → active;否则 waiting_period。 */
|
||||
function resolveInitialStatus(benefitStart?: string, benefitEnd?: string): string {
|
||||
if (benefitStart && benefitEnd) {
|
||||
const today = todayStr();
|
||||
if (benefitStart <= today && today <= benefitEnd) return 'active';
|
||||
}
|
||||
return 'waiting_period';
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD → UTC 零点 Date(Prisma @db.Date 按 UTC instant 取日期,须用 UTC 零点避免时区回退一天);空值返回 undefined。 */
|
||||
export function toDateOnly(v?: string): Date | undefined {
|
||||
if (!v) return undefined;
|
||||
return new Date(`${v}T00:00:00Z`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 老人档案(elder)业务服务。elder 是租户级软删模型,读写经 Prisma 扩展自动注入
|
||||
* tenantCode / deletedAt 过滤;创建时 tenantCode/createdBy 由 BaseCrudService 注入。
|
||||
* elder_status_log 为 append-only 租户级模型(无软删),tenantCode 由本 service 显式写入。
|
||||
*/
|
||||
@Injectable()
|
||||
export class ElderService extends BaseCrudService {
|
||||
constructor(
|
||||
prisma: PrismaService,
|
||||
private readonly storage: StorageService,
|
||||
private readonly auditLogs: AuditLogsService,
|
||||
) {
|
||||
super(prisma, 'elder');
|
||||
}
|
||||
|
||||
/** 分页列表:状态/等级/姓名筛选;身份证服务端掩码。 */
|
||||
async listElder(q: ListElderQuery) {
|
||||
const where: Record<string, unknown> = {};
|
||||
if (q.name) where.name = { contains: q.name };
|
||||
if (q.treatStatus) where.treatStatus = q.treatStatus;
|
||||
if (q.level) where.OR = [{ assessLevel: q.level }, { careLevel: q.level }];
|
||||
const page = await this.paginate(q.page, q.pageSize, where);
|
||||
return {
|
||||
...page,
|
||||
list: page.list.map((e: Record<string, unknown>) => ({ ...e, idCardNo: maskIdCard(String(e.idCardNo ?? '')) })),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 建档:校验身份证(18 位+校验位)→ 同机构防重 → 等级与副本绑定 →
|
||||
* 出生日期由身份证带出 → 初始状态由待遇有效期决定。
|
||||
*/
|
||||
async createElder(dto: CreateElderDto) {
|
||||
const idCardNo = dto.idCardNo.trim();
|
||||
const birthDate = deriveBirthDate(idCardNo);
|
||||
this.assertBenefitRange(dto.benefitStart, dto.benefitEnd);
|
||||
await this.assertIdCardUnique(idCardNo);
|
||||
if ((dto.assessLevel || dto.careLevel) && !dto.assessEvidenceFile) {
|
||||
throw new BusinessException(
|
||||
ErrorCodes.ELDER_LEVEL_REQUIRES_EVIDENCE,
|
||||
'填写等级须上传评估结论副本',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const treatStatus = resolveInitialStatus(dto.benefitStart, dto.benefitEnd);
|
||||
// TODO(id_card_no 加密): 本期直接存明文,加密机制由后续 security change 统一落地。
|
||||
return super.create({
|
||||
...dto,
|
||||
idCardNo,
|
||||
birthDate: toDateOnly(birthDate),
|
||||
treatStatus,
|
||||
statusSource: 'manual',
|
||||
benefitStart: toDateOnly(dto.benefitStart),
|
||||
benefitEnd: toDateOnly(dto.benefitEnd),
|
||||
assessDate: toDateOnly(dto.assessDate),
|
||||
insurVerifyDate: toDateOnly(dto.insurVerifyDate),
|
||||
hospStart: toDateOnly(dto.hospStart),
|
||||
hospEnd: toDateOnly(dto.hospEnd),
|
||||
});
|
||||
}
|
||||
|
||||
/** 详情:admin/qc(或平台超管)返回身份证明文并显式写审计;否则掩码。 */
|
||||
async detail(id: string) {
|
||||
const elder = await this.requireExists(id);
|
||||
if (await this.canViewPlainIdCard()) {
|
||||
const ctx = getRequestContext();
|
||||
await this.auditLogs.record({
|
||||
tenantCode: ctx?.tenantCode ?? '',
|
||||
userId: ctx?.userId ?? null,
|
||||
username: ctx?.username ?? null,
|
||||
module: 'elder',
|
||||
action: 'view-id-card',
|
||||
status: 'success',
|
||||
params: JSON.stringify({ elderId: id, elderName: elder.name }),
|
||||
});
|
||||
return elder;
|
||||
}
|
||||
return { ...elder, idCardNo: maskIdCard(String(elder.idCardNo ?? '')) };
|
||||
}
|
||||
|
||||
/** 变更待遇状态:changeReason 必填;写 elder_status_log(from/to/reason/operator/changedAt)。 */
|
||||
async changeStatus(id: string, dto: ChangeStatusDto) {
|
||||
if (!dto.changeReason) {
|
||||
throw new BusinessException(
|
||||
ErrorCodes.ELDER_STATUS_REASON_REQUIRED,
|
||||
'变更状态须填写原因',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
const elder = await this.requireExists(id);
|
||||
if (IRREVERSIBLE_TREAT_STATUSES.includes(elder.treatStatus)) {
|
||||
throw new BadRequestException('当前待遇状态不可再变更');
|
||||
}
|
||||
const ctx = getRequestContext();
|
||||
const fromStatus = elder.treatStatus;
|
||||
await this.update(id, {
|
||||
treatStatus: dto.toStatus,
|
||||
statusSource: 'manual',
|
||||
statusChangeDate: new Date(),
|
||||
});
|
||||
await this.prisma.client.elderStatusLog.create({
|
||||
data: {
|
||||
elderId: id,
|
||||
tenantCode: ctx?.tenantCode ?? '',
|
||||
fromStatus,
|
||||
toStatus: dto.toStatus,
|
||||
changeReason: dto.changeReason,
|
||||
source: 'manual',
|
||||
operatorId: ctx?.userId ?? '',
|
||||
evidenceFile: dto.evidenceFile ?? null,
|
||||
},
|
||||
});
|
||||
return { id, fromStatus, toStatus: dto.toStatus };
|
||||
}
|
||||
|
||||
/** 状态变更历史(按时间倒序)。 */
|
||||
statusLog(id: string) {
|
||||
const ctx = getRequestContext();
|
||||
return this.prisma.client.elderStatusLog.findMany({
|
||||
where: { elderId: id, tenantCode: ctx?.tenantCode },
|
||||
orderBy: { changedAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
/** 上传评估结论副本:读 buffer → StorageService.buildKey + upload → 返回 { key }。 */
|
||||
async uploadEvidence(file: { buffer: Buffer; originalname: string }) {
|
||||
const ctx = getRequestContext();
|
||||
const key = this.storage.buildKey(ctx?.tenantCode ?? '', file.originalname);
|
||||
await this.storage.upload(file.buffer, key);
|
||||
return { key };
|
||||
}
|
||||
|
||||
/** admin/qc 角色(或平台超管)可查看身份证明文。 */
|
||||
private async canViewPlainIdCard(): Promise<boolean> {
|
||||
const ctx = getRequestContext();
|
||||
if (!ctx) return false;
|
||||
if (ctx.super) return true;
|
||||
const rows = await this.prisma.client.userRole.findMany({
|
||||
where: { tenantCode: ctx.tenantCode, userId: ctx.userId },
|
||||
select: { roleCode: true },
|
||||
});
|
||||
return rows.some((r) => r.roleCode === 'admin' || r.roleCode === 'qc');
|
||||
}
|
||||
|
||||
private async assertIdCardUnique(idCardNo: string) {
|
||||
const existing = await this.db().findFirst({ where: { idCardNo } });
|
||||
if (existing) {
|
||||
throw new BusinessException(ErrorCodes.ELDER_ID_CARD_EXISTS, '已有档案', HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
/** 待遇有效期须「止 > 起」。 */
|
||||
private assertBenefitRange(benefitStart?: string, benefitEnd?: string) {
|
||||
if (benefitStart && benefitEnd && benefitEnd <= benefitStart) {
|
||||
throw new BadRequestException('待遇有效期止须晚于待遇有效期起');
|
||||
}
|
||||
}
|
||||
|
||||
private async requireExists(id: string): Promise<Record<string, any>> {
|
||||
const existing = await this.findById(id);
|
||||
if (!existing) {
|
||||
throw new BusinessException(ErrorCodes.ELDER_NOT_FOUND, '老人档案不存在', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ import { getRequestContext } from '../common/context/request-context.js';
|
||||
/** 不带 tenantCode 的模型(租户表自身)。 */
|
||||
const TENANT_FREE = new Set(['Tenant', 'Agency', 'SvcNational', 'SvcLocal', 'SvcMapping', 'LocConfig']);
|
||||
/** 软删模型:读操作统一注入 deletedAt IS NULL。 */
|
||||
const SOFT_DELETE = new Set(['User', 'Role', 'Resource', 'Org', 'DictType', 'DictItem', 'Config', 'Notice', 'Staff', 'SvcOrg']);
|
||||
const SOFT_DELETE = new Set(['User', 'Role', 'Resource', 'Org', 'DictType', 'DictItem', 'Config', 'Notice', 'Staff', 'SvcOrg', 'Elder']);
|
||||
const READ_OPS = new Set(['findMany', 'findFirst', 'findUnique', 'count', 'aggregate', 'groupBy']);
|
||||
/** 没有 where 子句的操作(create 的租户由 service 在 data 里显式写入)。 */
|
||||
const NO_WHERE_OPS = new Set(['create', 'createMany']);
|
||||
|
||||
8
src/storage/storage.module.ts
Normal file
8
src/storage/storage.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { StorageService } from './storage.service.js';
|
||||
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
exports: [StorageService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
20
src/storage/storage.service.spec.ts
Normal file
20
src/storage/storage.service.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { StorageService } from './storage.service.js';
|
||||
|
||||
describe('StorageService', () => {
|
||||
it('upload 写桶并返回 key', async () => {
|
||||
const putObject = vi.fn().mockResolvedValue({ etag: 'x' });
|
||||
const svc = new StorageService();
|
||||
(svc as any).client = { putObject };
|
||||
(svc as any).bucket = 'carelink';
|
||||
const key = await svc.upload(Buffer.from('x'), 'elder/evidence/a.jpg');
|
||||
expect(putObject).toHaveBeenCalledWith('carelink', 'elder/evidence/a.jpg', expect.any(Buffer), expect.any(Number));
|
||||
expect(key).toBe('elder/evidence/a.jpg');
|
||||
});
|
||||
|
||||
it('buildKey 带租户前缀', () => {
|
||||
const svc = new StorageService();
|
||||
const key = svc.buildKey('T001', 'a.jpg');
|
||||
expect(key).toMatch(/^elder\/T001\/\d+-a\.jpg$/);
|
||||
});
|
||||
});
|
||||
30
src/storage/storage.service.ts
Normal file
30
src/storage/storage.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Client } from 'minio';
|
||||
|
||||
@Injectable()
|
||||
export class StorageService {
|
||||
private readonly client: Client;
|
||||
private readonly bucket: string;
|
||||
|
||||
constructor() {
|
||||
// new Client() 只是构造客户端对象、不建立连接;putObject 时才连。
|
||||
// 测试里直接替换 (svc as any).client / .bucket 为 mock。
|
||||
this.client = new Client({
|
||||
endPoint: process.env.MINIO_ENDPOINT ?? 'localhost',
|
||||
port: Number(process.env.MINIO_PORT ?? 9000),
|
||||
useSSL: false,
|
||||
accessKey: process.env.MINIO_ACCESS_KEY ?? 'minioadmin',
|
||||
secretKey: process.env.MINIO_SECRET_KEY ?? 'minioadmin',
|
||||
});
|
||||
this.bucket = process.env.MINIO_BUCKET ?? 'carelink';
|
||||
}
|
||||
|
||||
buildKey(tenantCode: string, filename: string): string {
|
||||
return `elder/${tenantCode}/${Date.now()}-${filename}`;
|
||||
}
|
||||
|
||||
async upload(buffer: Buffer, key: string): Promise<string> {
|
||||
await this.client.putObject(this.bucket, key, buffer, buffer.length);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user