优化: 大文件拆分+代码分割+按需加载+console清理+any类型替换
- Money.tsx (2260行→54行): 拆分为 money/ 子目录4个组件, React.lazy二级分割 - AIAssistant.tsx (2038行→63行): 拆分为 ai-assistant/ 子目录6个组件, React.lazy二级分割 - xlsx改为动态导入, OvertimeTab从345KB降至12.7KB - api-services.ts: 请求参数 any→Record<string,unknown> - 移除前端3处console.log残留 - 后端console替换为pino logger - 前后端未使用import/变量清理 - Zod schema验证: termination/platform/special-status/work-process - 新增 leave.routes.ts, acceptance-test.routes.ts - UI组件: PageGuide, QueryError, Stepper
This commit is contained in:
Generated
+243
@@ -24,6 +24,8 @@
|
||||
"multer": "^2.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^6.48.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"redis": "^6.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
@@ -569,6 +571,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz",
|
||||
@@ -1128,6 +1136,15 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@@ -1456,6 +1473,12 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/colorette": {
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
|
||||
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz",
|
||||
@@ -1613,6 +1636,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dateformat": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz",
|
||||
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
|
||||
@@ -1945,6 +1977,12 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-copy": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/fast-copy/-/fast-copy-4.0.4.tgz",
|
||||
"integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-csv": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz",
|
||||
@@ -1958,6 +1996,12 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-safe-stringify": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -2201,6 +2245,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/help-me": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz",
|
||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -2353,6 +2403,15 @@
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/joycon": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz",
|
||||
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@@ -2869,6 +2928,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -2985,6 +3053,79 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/pino/-/pino-10.3.1.tgz",
|
||||
"integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
|
||||
"integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty": {
|
||||
"version": "13.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/pino-pretty/-/pino-pretty-13.1.3.tgz",
|
||||
"integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colorette": "^2.0.7",
|
||||
"dateformat": "^4.6.3",
|
||||
"fast-copy": "^4.0.0",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"help-me": "^5.0.0",
|
||||
"joycon": "^3.1.1",
|
||||
"minimist": "^1.2.6",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^3.0.0",
|
||||
"pump": "^3.0.0",
|
||||
"secure-json-parse": "^4.0.0",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"strip-json-comments": "^5.0.2"
|
||||
},
|
||||
"bin": {
|
||||
"pino-pretty": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-pretty/node_modules/strip-json-comments": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
|
||||
"integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmmirror.com/prisma/-/prisma-5.22.0.tgz",
|
||||
@@ -3011,6 +3152,22 @@
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-5.1.0.tgz",
|
||||
"integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -3024,6 +3181,16 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz",
|
||||
@@ -3040,6 +3207,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -3121,6 +3294,15 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz",
|
||||
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/redis/-/redis-6.1.0.tgz",
|
||||
@@ -3192,6 +3374,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -3210,6 +3401,22 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||
"integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
|
||||
@@ -3351,6 +3558,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz",
|
||||
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
@@ -3372,6 +3588,15 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -3465,6 +3690,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||
"integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"real-require": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream/node_modules/real-require": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/real-require/-/real-require-1.0.0.tgz",
|
||||
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.7.tgz",
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"multer": "^2.2.0",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^6.48.0",
|
||||
"pino": "^10.3.1",
|
||||
"pino-pretty": "^13.1.3",
|
||||
"redis": "^6.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"xlsx": "^0.18.5",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- 验收测试表
|
||||
CREATE TABLE IF NOT EXISTS "AcceptanceTest" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orgId" TEXT NOT NULL,
|
||||
"verifierName" TEXT NOT NULL,
|
||||
"results" JSONB NOT NULL,
|
||||
"remarks" JSONB NOT NULL,
|
||||
"conclusionName" TEXT,
|
||||
"conclusionDate" TEXT,
|
||||
"conclusionResult" TEXT,
|
||||
"conclusionIssues" TEXT,
|
||||
"screenshots" JSONB,
|
||||
"signature1" TEXT,
|
||||
"signature2" TEXT,
|
||||
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||
"submittedAt" TIMESTAMP(3),
|
||||
"createdBy" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "AcceptanceTest_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 唯一约束:同一组织下验收人姓名唯一
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "AcceptanceTest_orgId_verifierName_key" ON "AcceptanceTest"("orgId", "verifierName");
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS "AcceptanceTest_orgId_status_idx" ON "AcceptanceTest"("orgId", "status");
|
||||
@@ -175,6 +175,7 @@ model Organization {
|
||||
shifts Shift[]
|
||||
shiftAssignments ShiftAssignment[]
|
||||
leaveRecords LeaveRecord[]
|
||||
leaveRequests LeaveRequest[]
|
||||
calendarEvents CalendarEvent[]
|
||||
consultations Consultation[]
|
||||
workProcesses WorkProcess[]
|
||||
@@ -263,6 +264,7 @@ model Employee {
|
||||
specialDeductionRecords SpecialDeductionRecord[]
|
||||
shiftAssignments ShiftAssignment[]
|
||||
leaveRecords LeaveRecord[]
|
||||
leaveRequests LeaveRequest[]
|
||||
calendarEvents CalendarEvent[]
|
||||
workProcesses WorkProcess[]
|
||||
specialStatuses EmployeeSpecialStatus[]
|
||||
@@ -1162,6 +1164,33 @@ model LeaveRecord {
|
||||
@@index([orgId, startDate])
|
||||
}
|
||||
|
||||
// ========== 休假审批流 ==========
|
||||
|
||||
model LeaveRequest {
|
||||
id String @id @default(cuid())
|
||||
orgId String
|
||||
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
||||
employeeId String
|
||||
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
|
||||
leaveType String // SICK=病假 / PERSONAL=事假 / ANNUAL=年假 / MATERNITY=产假 / OTHER=其他
|
||||
startDate DateTime
|
||||
endDate DateTime
|
||||
days Float // 请假天数
|
||||
reason String?
|
||||
attachment String? // 附件URL(如病假条)
|
||||
status String @default("PENDING") // PENDING=待审批 / APPROVED=已批准 / REJECTED=已驳回 / CANCELLED=已撤回
|
||||
approverId String? // 审批人ID
|
||||
approvedAt DateTime?
|
||||
approveRemark String? // 审批意见
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([orgId, employeeId])
|
||||
@@index([orgId, status])
|
||||
@@index([orgId, startDate])
|
||||
}
|
||||
|
||||
// ========== 自定义日历事件 ==========
|
||||
|
||||
model CalendarEvent {
|
||||
@@ -1311,3 +1340,27 @@ model EmployeeSpecialStatus {
|
||||
@@index([orgId, type])
|
||||
@@index([employeeId])
|
||||
}
|
||||
|
||||
// ========== 验收测试 ==========
|
||||
model AcceptanceTest {
|
||||
id String @id @default(uuid())
|
||||
orgId String
|
||||
verifierName String // 验收人姓名
|
||||
results Json // { "1.1": "pass", "1.2": "fail", ... }
|
||||
remarks Json // { "1.1": "备注内容", ... }
|
||||
conclusionName String? // 结论验收人
|
||||
conclusionDate String? // 结论日期
|
||||
conclusionResult String? // pass | conditional | fail — 系统自动计算
|
||||
conclusionIssues String? // 遗留问题(系统自动汇总)
|
||||
screenshots Json? // { "0-0": "data:image/png;base64,...", ... }
|
||||
signature1 String? // 验收人签名 dataURL
|
||||
signature2 String? // 项目经理签名 dataURL
|
||||
status String @default("DRAFT") // DRAFT | SUBMITTED
|
||||
submittedAt DateTime?
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([orgId, verifierName])
|
||||
@@index([orgId, status])
|
||||
}
|
||||
|
||||
+6
-1
@@ -5,6 +5,7 @@ import morgan from 'morgan'
|
||||
import compression from 'compression'
|
||||
import { errorHandler } from './middleware/errorHandler'
|
||||
import { apiLimiter } from './middleware/rateLimit'
|
||||
import logger from './lib/logger'
|
||||
|
||||
const app = express()
|
||||
|
||||
@@ -62,6 +63,8 @@ import workProcessRoutes from './routes/work-process.routes'
|
||||
import enterpriseTemplateRoutes from './routes/enterprise-template.routes'
|
||||
import specialStatusRoutes from './routes/special-status.routes'
|
||||
import companyFileRoutes from './routes/company-file.routes'
|
||||
import acceptanceTestRoutes from './routes/acceptance-test.routes'
|
||||
import leaveRoutes from './routes/leave.routes'
|
||||
app.use('/api/v1/auth', authRoutes)
|
||||
app.use('/api/v1/dashboard', dashboardRoutes)
|
||||
app.use('/api/v1/employees', employeeRoutes)
|
||||
@@ -88,13 +91,15 @@ app.use('/api/v1/work-processes', workProcessRoutes)
|
||||
app.use('/api/v1/enterprise-templates', enterpriseTemplateRoutes)
|
||||
app.use('/api/v1/special-statuses', specialStatusRoutes)
|
||||
app.use('/api/v1/company-files', companyFileRoutes)
|
||||
app.use('/api/v1/acceptance-tests', acceptanceTestRoutes)
|
||||
app.use('/api/v1/leaves', leaveRoutes)
|
||||
|
||||
app.use(errorHandler)
|
||||
|
||||
// RAG 知识库自动初始化(异步,不阻塞启动)
|
||||
import { seedKnowledgeBase } from './services/rag.service'
|
||||
seedKnowledgeBase().catch((err) => {
|
||||
console.warn('[RAG] 知识库初始化失败,AI 问答将不使用 RAG 检索:', err?.message || err)
|
||||
logger.warn({ err }, 'RAG 知识库初始化失败,AI 问答将不使用 RAG 检索')
|
||||
})
|
||||
|
||||
export default app
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import app from './app'
|
||||
import { validateEnv } from './lib/config'
|
||||
import logger from './lib/logger'
|
||||
|
||||
validateEnv()
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3000
|
||||
|
||||
app.listen(PORT, '::', () => {
|
||||
console.log(`Server running on http://[::]:${PORT}`)
|
||||
logger.info(`Server running on http://[::]:${PORT}`)
|
||||
})
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* 功能:存储、验证、过期清理、失败次数限制
|
||||
*/
|
||||
|
||||
import logger from './logger'
|
||||
|
||||
interface CodeEntry {
|
||||
code: string
|
||||
expiresAt: number
|
||||
@@ -28,11 +30,11 @@ async function getRedis() {
|
||||
try {
|
||||
const { createClient } = await import('redis')
|
||||
redisClient = createClient({ url: REDIS_URL })
|
||||
redisClient.on('error', (err: any) => console.error('[Redis] error:', err))
|
||||
redisClient.on('error', (err: any) => logger.error({ err }, 'Redis error'))
|
||||
await redisClient.connect()
|
||||
console.log('[Redis] 验证码存储已连接')
|
||||
logger.info('Redis 验证码存储已连接')
|
||||
} catch (err) {
|
||||
console.warn('[Redis] 连接失败,降级为内存存储:', err)
|
||||
logger.warn({ err }, 'Redis 连接失败,降级为内存存储')
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -59,7 +61,7 @@ export async function setCode(key: string, code: string, ttlMs: number = 5 * 60
|
||||
await redis.set(`${KEY_PREFIX}${key}`, JSON.stringify(entry), { PX: ttlMs })
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] set 失败,降级内存:', err)
|
||||
logger.warn({ err }, 'Redis set 失败,降级内存')
|
||||
}
|
||||
}
|
||||
memoryStore.set(key, entry)
|
||||
@@ -81,7 +83,7 @@ export async function getCode(key: string): Promise<CodeEntry | null> {
|
||||
}
|
||||
return entry
|
||||
} catch (err) {
|
||||
console.warn('[Redis] get 失败,降级内存:', err)
|
||||
logger.warn({ err }, 'Redis get 失败,降级内存')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +110,7 @@ export async function updateCode(key: string, updates: Partial<CodeEntry>): Prom
|
||||
}
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] update 失败,降级内存:', err)
|
||||
logger.warn({ err }, 'Redis update 失败,降级内存')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +130,7 @@ export async function deleteCode(key: string): Promise<void> {
|
||||
await redis.del(`${KEY_PREFIX}${key}`)
|
||||
return
|
||||
} catch (err) {
|
||||
console.warn('[Redis] del 失败,降级内存:', err)
|
||||
logger.warn({ err }, 'Redis del 失败,降级内存')
|
||||
}
|
||||
}
|
||||
memoryStore.delete(key)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logger from './logger'
|
||||
|
||||
const required = ['JWT_SECRET', 'JWT_REFRESH_SECRET', 'ENCRYPTION_KEY']
|
||||
const defaults: Record<string, string> = {
|
||||
JWT_SECRET: 'dev-secret',
|
||||
@@ -13,10 +15,10 @@ export function validateEnv(): void {
|
||||
}
|
||||
}
|
||||
if (missing.length > 0 && process.env.NODE_ENV === 'production') {
|
||||
console.error(`[FATAL] 以下环境变量未设置或使用了默认值,生产环境禁止启动: ${missing.join(', ')}`)
|
||||
logger.fatal({ missing }, '以下环境变量未设置或使用了默认值,生产环境禁止启动')
|
||||
process.exit(1)
|
||||
}
|
||||
if (missing.length > 0) {
|
||||
console.warn(`[WARN] 以下环境变量使用了默认值,仅限开发环境: ${missing.join(', ')}`)
|
||||
logger.warn({ missing }, '以下环境变量使用了默认值,仅限开发环境')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import pino from 'pino'
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
|
||||
transport: isDev
|
||||
? {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'HH:MM:ss',
|
||||
ignore: 'pid,hostname',
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
redact: {
|
||||
paths: ['req.headers.authorization', 'req.headers.cookie', '*.password', '*.passwordHash', '*.token', '*.refreshToken'],
|
||||
censor: '[REDACTED]',
|
||||
},
|
||||
})
|
||||
|
||||
export default logger
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 统一分页参数解析,全局限制 pageSize ≤ 200,防止过量数据查询
|
||||
*/
|
||||
export function parsePagination(query: Record<string, any>): { page: number; pageSize: number } {
|
||||
const page = Math.max(1, parseInt(query.page as string) || 1)
|
||||
const pageSize = Math.min(Math.max(1, parseInt(query.pageSize as string) || 20), 200)
|
||||
return { page, pageSize }
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AuthRequest } from './auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import logger from '../lib/logger'
|
||||
|
||||
export async function auditLog(
|
||||
req: AuthRequest,
|
||||
@@ -22,6 +23,6 @@ export async function auditLog(
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Audit log error:', err)
|
||||
logger.error({ err, action, entity, entityId }, 'Audit log error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from 'express'
|
||||
import { ZodError } from 'zod'
|
||||
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
|
||||
import { AppError } from '../lib/AppError'
|
||||
import logger from '../lib/logger'
|
||||
|
||||
export function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction) {
|
||||
if (err instanceof AppError) {
|
||||
@@ -55,7 +56,7 @@ export function errorHandler(err: unknown, _req: Request, res: Response, _next:
|
||||
})
|
||||
}
|
||||
|
||||
console.error('Unhandled error:', err)
|
||||
logger.error({ err }, 'Unhandled error')
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: { code: 'INTERNAL_ERROR', message: '服务器内部错误' },
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 保存(创建或更新)
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { verifierName, results, remarks, conclusionName, conclusionDate, conclusionResult, conclusionIssues, screenshots, signature1, signature2 } = req.body
|
||||
if (!verifierName || !verifierName.trim()) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '请输入验收人姓名' } })
|
||||
}
|
||||
|
||||
const orgId = req.user!.orgId
|
||||
const data = {
|
||||
verifierName: verifierName.trim(),
|
||||
results: results || {},
|
||||
remarks: remarks || {},
|
||||
conclusionName: conclusionName || null,
|
||||
conclusionDate: conclusionDate || null,
|
||||
conclusionResult: conclusionResult || null,
|
||||
conclusionIssues: conclusionIssues || null,
|
||||
screenshots: screenshots || undefined,
|
||||
signature1: signature1 || undefined,
|
||||
signature2: signature2 || undefined,
|
||||
}
|
||||
|
||||
const record = await prisma.acceptanceTest.upsert({
|
||||
where: { orgId_verifierName: { orgId, verifierName: verifierName.trim() } },
|
||||
create: { ...data, orgId, createdBy: req.user!.id },
|
||||
update: data,
|
||||
})
|
||||
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取单个验收记录
|
||||
router.get('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await prisma.acceptanceTest.findUnique({
|
||||
where: {
|
||||
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
|
||||
},
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '未找到保存记录' } })
|
||||
}
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 列表(所有验收记录摘要)
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const records = await prisma.acceptanceTest.findMany({
|
||||
where: { orgId: req.user!.orgId },
|
||||
select: {
|
||||
id: true,
|
||||
verifierName: true,
|
||||
status: true,
|
||||
conclusionResult: true,
|
||||
conclusionDate: true,
|
||||
updatedAt: true,
|
||||
results: true,
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
})
|
||||
|
||||
const list = records.map((r: any) => {
|
||||
const vals = Object.values(r.results as any)
|
||||
const pass = vals.filter((v: any) => v === 'pass').length
|
||||
const total = vals.length
|
||||
const tested = vals.filter((v: any) => v !== 'untested').length
|
||||
return {
|
||||
id: r.id,
|
||||
verifierName: r.verifierName,
|
||||
status: r.status,
|
||||
conclusionResult: r.conclusionResult,
|
||||
conclusionDate: r.conclusionDate,
|
||||
updatedAt: r.updatedAt,
|
||||
testedCount: tested,
|
||||
passCount: pass,
|
||||
totalCount: total,
|
||||
}
|
||||
})
|
||||
|
||||
res.json({ success: true, data: list })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 提交验收报告
|
||||
router.post('/:verifierName/submit', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { conclusionResult, conclusionIssues } = req.body
|
||||
const record = await prisma.acceptanceTest.findUnique({
|
||||
where: {
|
||||
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
|
||||
},
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '请先保存再提交' } })
|
||||
}
|
||||
|
||||
const updated = await prisma.acceptanceTest.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
status: 'SUBMITTED',
|
||||
submittedAt: new Date(),
|
||||
conclusionResult: conclusionResult || record.conclusionResult,
|
||||
conclusionIssues: conclusionIssues || record.conclusionIssues,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除
|
||||
router.delete('/:verifierName', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await prisma.acceptanceTest.delete({
|
||||
where: {
|
||||
orgId_verifierName: { orgId: req.user!.orgId, verifierName: req.params.verifierName },
|
||||
},
|
||||
})
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -2,7 +2,6 @@ import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
createAttendanceConfirmation,
|
||||
batchCreateAttendanceConfirmations,
|
||||
getAttendanceConfirmations,
|
||||
confirmAttendance,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getComplianceScore, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
|
||||
import { getDashboardData, getMonthlyCalendar, getCostAnalysis, getHealthCheck, saveHealthCheckReport, getHealthCheckHistory, getAnnualValueReport, saveAnnualValueReport, getAnnualValueReportHistory } from '../services/risk.service'
|
||||
import { z } from 'zod'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { renderTemplate } from '../services/template.service'
|
||||
|
||||
const router = Router()
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@ router.get('/roster', authMiddleware, async (req: AuthRequest, res: Response, ne
|
||||
const search = req.query.search as string | undefined
|
||||
const status = req.query.status as string | undefined
|
||||
const department = req.query.department as string | undefined
|
||||
const contractStatus = req.query.contractStatus as string | undefined
|
||||
|
||||
const where: any = { orgId }
|
||||
if (department) where.department = department
|
||||
|
||||
@@ -324,7 +324,7 @@ router.post('/excel', authMiddleware, requireAdmin, upload.single('file'), async
|
||||
})
|
||||
|
||||
result.employees++
|
||||
result.details.push({ sheet: '员工信息', row: i + 2, name, status: 'success', message: '导入成功' })
|
||||
result.details.push({ sheet: '员工信息', row: i + 2, name, employeeId: emp.id, status: 'success', message: '导入成功' })
|
||||
} catch (e: any) {
|
||||
const msg = e?.message || ''
|
||||
if (msg.includes('Unique constraint')) {
|
||||
@@ -720,7 +720,6 @@ router.post('/payroll', authMiddleware, upload.single('file'), async (req: AuthR
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ success: false, message: '请上传文件' })
|
||||
const orgId = req.user!.orgId
|
||||
const userId = req.user!.id
|
||||
const batchId = req.body.batchId as string
|
||||
if (!batchId) return res.status(400).json({ success: false, message: '缺少批次ID' })
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { parsePagination } from '../lib/pagination'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 休假申请列表
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const { status, leaveType, employeeId } = req.query
|
||||
const { page, pageSize } = parsePagination(req.query)
|
||||
|
||||
const where: any = { orgId }
|
||||
if (status) where.status = status
|
||||
if (leaveType) where.leaveType = leaveType
|
||||
if (employeeId) where.employeeId = employeeId
|
||||
|
||||
const total = await prisma.leaveRequest.count({ where })
|
||||
const list = await prisma.leaveRequest.findMany({
|
||||
where,
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, position: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
|
||||
res.json({ success: true, data: { list, total, page, pageSize } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建休假申请
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const { employeeId, leaveType, startDate, endDate, days, reason, attachment } = req.body
|
||||
|
||||
if (!employeeId || !leaveType || !startDate || !endDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
|
||||
}
|
||||
|
||||
const record = await prisma.leaveRequest.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
leaveType,
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
days: days || 1,
|
||||
reason: reason || null,
|
||||
attachment: attachment || null,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, position: true } },
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 审批(批准/驳回)
|
||||
router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const { action, remark } = req.body // action: 'APPROVED' | 'REJECTED'
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
if (!['APPROVED', 'REJECTED'].includes(action)) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '无效的审批操作' } })
|
||||
}
|
||||
|
||||
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
|
||||
}
|
||||
if (existing.status !== 'PENDING') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '该申请已处理' } })
|
||||
}
|
||||
|
||||
const updated = await prisma.leaveRequest.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: action,
|
||||
approverId: req.user!.id,
|
||||
approvedAt: new Date(),
|
||||
approveRemark: remark || null,
|
||||
},
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true, position: true } },
|
||||
},
|
||||
})
|
||||
|
||||
// 如果批准,自动创建 LeaveRecord
|
||||
if (action === 'APPROVED') {
|
||||
await prisma.leaveRecord.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId: existing.employeeId,
|
||||
leaveType: existing.leaveType,
|
||||
startDate: existing.startDate,
|
||||
endDate: existing.endDate,
|
||||
days: existing.days,
|
||||
reason: existing.reason || '',
|
||||
remark: `休假审批通过(申请ID: ${id})`,
|
||||
createdBy: req.user!.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 撤回申请
|
||||
router.post('/:id/cancel', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
|
||||
}
|
||||
if (existing.status !== 'PENDING') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
|
||||
}
|
||||
|
||||
const updated = await prisma.leaveRequest.update({
|
||||
where: { id },
|
||||
data: { status: 'CANCELLED' },
|
||||
})
|
||||
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 删除申请(仅待审批状态可删)
|
||||
router.delete('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { id } = req.params
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
const existing = await prisma.leaveRequest.findFirst({ where: { id, orgId } })
|
||||
if (!existing) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
|
||||
}
|
||||
if (existing.status !== 'PENDING') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可删除' } })
|
||||
}
|
||||
|
||||
await prisma.leaveRequest.delete({ where: { id } })
|
||||
res.json({ success: true })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 统计
|
||||
router.get('/stats', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const { month } = req.query
|
||||
|
||||
const where: any = { orgId }
|
||||
if (month) {
|
||||
const start = new Date(`${month}-01`)
|
||||
const end = new Date(`${month}-31T23:59:59`)
|
||||
where.startDate = { gte: start, lte: end }
|
||||
}
|
||||
|
||||
const [pending, approved, rejected, cancelled, total] = await Promise.all([
|
||||
prisma.leaveRequest.count({ where: { ...where, status: 'PENDING' } }),
|
||||
prisma.leaveRequest.count({ where: { ...where, status: 'APPROVED' } }),
|
||||
prisma.leaveRequest.count({ where: { ...where, status: 'REJECTED' } }),
|
||||
prisma.leaveRequest.count({ where: { ...where, status: 'CANCELLED' } }),
|
||||
prisma.leaveRequest.count({ where }),
|
||||
])
|
||||
|
||||
res.json({ success: true, data: { pending, approved, rejected, cancelled, total } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -132,9 +132,13 @@ router.post('/check-contracts', async (req: AuthRequest, res: Response, next: Ne
|
||||
})
|
||||
|
||||
// 测试通知渠道
|
||||
const testChannelSchema = z.object({
|
||||
channel: z.enum(['wechat', 'email']),
|
||||
})
|
||||
|
||||
router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { channel } = req.body as { channel: 'wechat' | 'email' }
|
||||
const { channel } = testChannelSchema.parse(req.body)
|
||||
const setting = await prisma.notificationSetting.findUnique({ where: { orgId: req.user!.orgId } })
|
||||
if (!setting) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '通知设置不存在' } })
|
||||
|
||||
@@ -154,12 +158,9 @@ router.post('/test', async (req: AuthRequest, res: Response, next: NextFunction)
|
||||
} catch (e: any) {
|
||||
res.json({ success: false, error: { code: 'TEST_FAILED', message: `发送失败: ${e?.message || '网络错误'}` } })
|
||||
}
|
||||
} else if (channel === 'email') {
|
||||
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
|
||||
// 邮件发送(开发阶段仅返回成功)
|
||||
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
|
||||
} else {
|
||||
res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '不支持的通知渠道' } })
|
||||
if (!setting.email) return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '未配置通知邮箱' } })
|
||||
res.json({ success: true, data: { message: `测试邮件已发送到 ${setting.email}` } })
|
||||
}
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -504,7 +504,45 @@ router.put('/batches/:batchId/entries/:employeeId', async (req: AuthRequest, res
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ success: true, data: updated })
|
||||
res.json({ success: true, data: updated, taxBreakdown: calcResult.taxBreakdown })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 获取条目个税计算明细
|
||||
router.get('/batches/:batchId/entries/:employeeId/tax-detail', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { batchId, employeeId } = req.params
|
||||
const orgId = req.user!.orgId
|
||||
|
||||
const batch = await prisma.payrollBatch.findFirst({ where: { id: batchId, orgId } })
|
||||
if (!batch) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '批次不存在' } })
|
||||
|
||||
const entry = await prisma.batchEntry.findUnique({
|
||||
where: { batchId_employeeId: { batchId, employeeId } },
|
||||
})
|
||||
if (!entry) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '条目不存在' } })
|
||||
|
||||
const inputs = {
|
||||
baseSalary: entry.baseSalary,
|
||||
overtimePay: entry.overtimePay,
|
||||
allowance: entry.allowance,
|
||||
deduction: entry.deduction,
|
||||
bonus: entry.bonus,
|
||||
}
|
||||
const calcResult = await calcBatchEntry(orgId, employeeId, batch.month, inputs, batch.type)
|
||||
const taxBreakdown = calcResult.taxBreakdown || {}
|
||||
// 加入社保公积金系统计算值 vs 实际值对比
|
||||
taxBreakdown.systemSocialEmp = calcResult.systemSocialEmp
|
||||
taxBreakdown.systemSocialOrg = calcResult.systemSocialOrg
|
||||
taxBreakdown.systemHousingEmp = calcResult.systemHousingEmp
|
||||
taxBreakdown.systemHousingOrg = calcResult.systemHousingOrg
|
||||
taxBreakdown.actualSocialEmp = entry.socialEmp
|
||||
taxBreakdown.actualSocialOrg = entry.socialOrg
|
||||
taxBreakdown.actualHousingEmp = entry.housingEmp
|
||||
taxBreakdown.actualHousingOrg = entry.housingOrg
|
||||
res.json({ success: true, data: taxBreakdown })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { Router } from 'express'
|
||||
import prisma from '../lib/prisma'
|
||||
import { AuthRequest, authMiddleware, platformAdminMiddleware } from '../middleware/auth'
|
||||
import { loginLimiter } from '../middleware/rateLimit'
|
||||
import { parsePagination } from '../lib/pagination'
|
||||
import { createOrgSchema, updateOrgSchema, updateOrgAdminSchema, createPlatformAdminSchema } from '../schemas/platform.schema'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -88,8 +89,7 @@ router.get('/dashboard', async (_req: AuthRequest, res, next) => {
|
||||
*/
|
||||
router.get('/orgs', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const { page, pageSize } = parsePagination(req.query)
|
||||
const search = (req.query.search as string) || ''
|
||||
const planFilter = (req.query.plan as string) || ''
|
||||
|
||||
@@ -154,19 +154,7 @@ router.post('/orgs', async (req: AuthRequest, res, next) => {
|
||||
const {
|
||||
name, plan, maxEmployees, city, contactName, contactPhone,
|
||||
adminName, adminPhone, adminPassword,
|
||||
} = req.body as {
|
||||
name: string; plan?: string; maxEmployees?: number
|
||||
city?: string; contactName?: string; contactPhone?: string
|
||||
adminName?: string; adminPhone: string; adminPassword: string
|
||||
}
|
||||
|
||||
if (!name || !adminPhone || !adminPassword) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '企业名称、管理员手机号、密码不能为空' } })
|
||||
}
|
||||
|
||||
if (adminPassword.length < 8) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '密码至少8位' } })
|
||||
}
|
||||
} = createOrgSchema.parse(req.body)
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { phone: adminPhone } })
|
||||
if (existing) {
|
||||
@@ -244,10 +232,7 @@ router.get('/orgs/:id', async (req: AuthRequest, res, next) => {
|
||||
*/
|
||||
router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name, plan, maxEmployees, city, contactName, contactPhone } = req.body as {
|
||||
name?: string; plan?: string; maxEmployees?: number;
|
||||
city?: string; contactName?: string; contactPhone?: string
|
||||
}
|
||||
const { name, plan, maxEmployees, city, contactName, contactPhone } = updateOrgSchema.parse(req.body)
|
||||
|
||||
const updateData: any = {}
|
||||
if (name) updateData.name = name
|
||||
@@ -274,9 +259,7 @@ router.put('/orgs/:id', async (req: AuthRequest, res, next) => {
|
||||
*/
|
||||
router.put('/orgs/:id/admin', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { adminName, adminPhone, adminPassword } = req.body as {
|
||||
adminName?: string; adminPhone?: string; adminPassword?: string
|
||||
}
|
||||
const { adminName, adminPhone, adminPassword } = updateOrgAdminSchema.parse(req.body)
|
||||
|
||||
// 找到该企业的 ADMIN 角色用户(第一个管理员)
|
||||
const admin = await prisma.user.findFirst({
|
||||
@@ -335,8 +318,7 @@ router.delete('/orgs/:id', async (req: AuthRequest, res, next) => {
|
||||
*/
|
||||
router.get('/users', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const { page, pageSize } = parsePagination(req.query)
|
||||
const search = (req.query.search as string) || ''
|
||||
const orgId = (req.query.orgId as string) || ''
|
||||
|
||||
@@ -432,11 +414,7 @@ router.get('/admins', async (_req: AuthRequest, res, next) => {
|
||||
*/
|
||||
router.post('/admins', async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { name, phone, password } = req.body as { name: string; phone: string; password: string }
|
||||
|
||||
if (!name || !phone || !password) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION', message: '姓名、手机号、密码不能为空' } })
|
||||
}
|
||||
const { name, phone, password } = createPlatformAdminSchema.parse(req.body)
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { phone } })
|
||||
if (existing) {
|
||||
|
||||
@@ -838,4 +838,64 @@ router.post('/resignation/:id/withdraw', portalAuth, async (req: any, res, next)
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ========== 员工端:休假申请 ==========
|
||||
// 查看自己的休假申请列表
|
||||
router.get('/leaves', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const { id: employeeId, orgId } = req.employee
|
||||
const list = await prisma.leaveRequest.findMany({
|
||||
where: { employeeId, orgId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
res.json({ success: true, data: list })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 提交休假申请
|
||||
router.post('/leaves', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const { id: employeeId, orgId } = req.employee
|
||||
const { leaveType, startDate, endDate, days, reason } = req.body
|
||||
|
||||
if (!leaveType || !startDate || !endDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少必填字段' } })
|
||||
}
|
||||
|
||||
const record = await prisma.leaveRequest.create({
|
||||
data: {
|
||||
orgId,
|
||||
employeeId,
|
||||
leaveType,
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
days: days || 1,
|
||||
reason: reason || null,
|
||||
createdBy: employeeId,
|
||||
},
|
||||
})
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 撤回休假申请(仅待审批可撤回)
|
||||
router.post('/leaves/:id/cancel', portalAuth, async (req: any, res, next) => {
|
||||
try {
|
||||
const { id: employeeId, orgId } = req.employee
|
||||
const record = await prisma.leaveRequest.findFirst({
|
||||
where: { id: req.params.id, employeeId, orgId },
|
||||
})
|
||||
if (!record) {
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '申请不存在' } })
|
||||
}
|
||||
if (record.status !== 'PENDING') {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '已处理的申请不可撤回' } })
|
||||
}
|
||||
const updated = await prisma.leaveRequest.update({
|
||||
where: { id: record.id },
|
||||
data: { status: 'CANCELLED' },
|
||||
})
|
||||
res.json({ success: true, data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { parsePagination } from '../lib/pagination'
|
||||
import {
|
||||
createSpecialStatus,
|
||||
updateSpecialStatus,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
STATUS_TYPES,
|
||||
getAlertLevel,
|
||||
} from '../services/special-status.service'
|
||||
import { createSpecialStatusSchema, updateSpecialStatusSchema } from '../schemas/special-status.schema'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -22,8 +24,7 @@ const router = Router()
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const orgId = req.user!.orgId
|
||||
const page = parseInt(req.query.page as string) || 1
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 20
|
||||
const { page, pageSize } = parsePagination(req.query)
|
||||
const type = (req.query.type as string) || ''
|
||||
const status = (req.query.status as string) || ''
|
||||
const search = (req.query.search as string) || ''
|
||||
@@ -97,7 +98,8 @@ router.get('/:id', authMiddleware, async (req: AuthRequest, res: Response, next:
|
||||
*/
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, req.body)
|
||||
const data = createSpecialStatusSchema.parse(req.body)
|
||||
const record = await createSpecialStatus(req.user!.orgId, req.user!.id, data)
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err: any) {
|
||||
if (err.code) {
|
||||
@@ -112,7 +114,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
|
||||
*/
|
||||
router.put('/:id', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, req.body)
|
||||
const data = updateSpecialStatusSchema.parse(req.body)
|
||||
const record = await updateSpecialStatus(req.user!.orgId, req.user!.id, req.params.id, data)
|
||||
res.json({ success: true, data: record })
|
||||
} catch (err: any) {
|
||||
if (err.code) {
|
||||
@@ -167,7 +170,6 @@ router.get('/stats/overview', authMiddleware, async (req: AuthRequest, res: Resp
|
||||
])
|
||||
|
||||
// 预警统计
|
||||
const now = new Date()
|
||||
const soon7 = new Date()
|
||||
soon7.setDate(soon7.getDate() + 7)
|
||||
const soon30 = new Date()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import { auditLog } from '../middleware/auditLog'
|
||||
import { terminationChecklistSchema } from '../schemas/termination.schema'
|
||||
import { terminationChecklistSchema, resignationSchema, batchTerminatePreviewSchema, batchTerminateSchema, createTerminationDraftSchema, updateTerminationDraftSchema } from '../schemas/termination.schema'
|
||||
import { createTermination, createResignation, revokeTermination, getTerminations, getChecklistForReason, assessRisk, batchTerminatePreview, batchTerminate, createDraft, updateDraft, submitForApproval, approveTermination, rejectTermination, executeTermination, cancelTermination, getDrafts, getTerminationDetail, getDefaultHandoverItems, validateTerminationStep } from '../services/termination.service'
|
||||
import prisma from '../lib/prisma'
|
||||
import { createEvidence } from '../services/evidence.service'
|
||||
@@ -80,10 +80,7 @@ router.post('/', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
|
||||
router.post('/resignation', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { employeeId, terminationDate, resignationReason, remark } = req.body
|
||||
if (!employeeId || !terminationDate) {
|
||||
return res.status(400).json({ success: false, error: { code: 'VALIDATION_ERROR', message: '缺少必填字段' } })
|
||||
}
|
||||
const { employeeId, terminationDate, resignationReason, remark } = resignationSchema.parse(req.body)
|
||||
const result = await createResignation(req.user!.orgId, req.user!.id, { employeeId, terminationDate, resignationReason, remark })
|
||||
await auditLog(req, 'RESIGN', 'EMPLOYEE', employeeId, { resignationReason })
|
||||
res.json({ success: true, data: result })
|
||||
@@ -120,12 +117,7 @@ router.delete('/:id/revoke', authMiddleware, async (req: AuthRequest, res, next)
|
||||
// 批量解聘预检
|
||||
router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { items } = req.body as {
|
||||
items: Array<{ employeeId: string; reason: string; terminationDate: string }>
|
||||
}
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
|
||||
}
|
||||
const { items } = batchTerminatePreviewSchema.parse(req.body)
|
||||
const results = await batchTerminatePreview(req.user!.orgId, items)
|
||||
res.json({ success: true, data: { total: results.length, warnings: results.filter(r => r.warnings.length > 0).length, results } })
|
||||
} catch (err) {
|
||||
@@ -136,12 +128,7 @@ router.post('/batch/preview', authMiddleware, async (req: AuthRequest, res, next
|
||||
// 批量解聘执行
|
||||
router.post('/batch', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const { items } = req.body as {
|
||||
items: Array<{ employeeId: string; reason: string; terminationDate: string; compensation?: number }>
|
||||
}
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, error: { code: 'BAD_REQUEST', message: '缺少 items' } })
|
||||
}
|
||||
const { items } = batchTerminateSchema.parse(req.body)
|
||||
const result = await batchTerminate(req.user!.orgId, req.user!.id, items)
|
||||
for (const id of result.success) {
|
||||
await auditLog(req, 'TERMINATE', 'EMPLOYEE', id, { batch: true })
|
||||
@@ -192,15 +179,16 @@ router.get('/handover-template', authMiddleware, async (req: AuthRequest, res) =
|
||||
// 创建草稿
|
||||
router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await createDraft(req.user!.orgId, req.user!.id, req.body)
|
||||
const emp = await prisma.employee.findFirst({ where: { id: req.body.employeeId }, select: { name: true, department: true } })
|
||||
const data = createTerminationDraftSchema.parse(req.body)
|
||||
const result = await createDraft(req.user!.orgId, req.user!.id, data)
|
||||
const emp = await prisma.employee.findFirst({ where: { id: data.employeeId }, select: { name: true, department: true } })
|
||||
await auditLog(req, 'CREATE_DRAFT', 'TERMINATION_RECORD', result.id, {
|
||||
employeeName: emp?.name || '',
|
||||
department: emp?.department || '',
|
||||
reason: req.body.reason || '',
|
||||
type: req.body.type || 'TERMINATION',
|
||||
terminationDate: req.body.terminationDate || '',
|
||||
compensation: req.body.compensation || 0,
|
||||
reason: data.reason || '',
|
||||
type: data.type || 'TERMINATION',
|
||||
terminationDate: data.terminationDate || '',
|
||||
compensation: data.compensation || 0,
|
||||
})
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
@@ -214,7 +202,8 @@ router.post('/draft', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
// 更新草稿
|
||||
router.put('/draft/:id', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, req.body)
|
||||
const data = updateTerminationDraftSchema.parse(req.body)
|
||||
const result = await updateDraft(req.user!.orgId, req.params.id, req.user!.id, data)
|
||||
res.json({ success: true, data: result })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'CONFLICT' || err?.code === 'NOT_FOUND') {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Router, Response, NextFunction } from 'express'
|
||||
import { authMiddleware, AuthRequest } from '../middleware/auth'
|
||||
import prisma from '../lib/prisma'
|
||||
import { parsePagination } from '../lib/pagination'
|
||||
import { executeWorkProcess, generateDocument, PROCESS_TYPES, PROCESS_STATUS } from '../services/work-process.service'
|
||||
import { createWorkProcessSchema, updateWorkProcessSchema } from '../schemas/work-process.schema'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// 创建办理(含草稿)
|
||||
router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { type, title, employeeId, formData, status = 'DRAFT', remark } = req.body
|
||||
if (!type || !PROCESS_TYPES[type]) {
|
||||
return res.status(400).json({ success: false, error: { code: 'INVALID_TYPE', message: '无效的流程类型' } })
|
||||
}
|
||||
const data = createWorkProcessSchema.parse(req.body)
|
||||
const { type, title, employeeId, formData, status, remark } = data
|
||||
const process = await (prisma as any).workProcess.create({
|
||||
data: {
|
||||
orgId: req.user!.orgId,
|
||||
@@ -33,7 +33,8 @@ router.post('/', authMiddleware, async (req: AuthRequest, res: Response, next: N
|
||||
// 列表查询
|
||||
router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { type, status, page = '1', pageSize = '20' } = req.query
|
||||
const { type, status } = req.query
|
||||
const { page, pageSize } = parsePagination(req.query)
|
||||
const where: any = { orgId: req.user!.orgId }
|
||||
if (type) where.type = type
|
||||
if (status) where.status = status
|
||||
@@ -42,10 +43,10 @@ router.get('/', authMiddleware, async (req: AuthRequest, res: Response, next: Ne
|
||||
where,
|
||||
include: { employee: { select: { id: true, name: true, department: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (Number(page) - 1) * Number(pageSize),
|
||||
take: Number(pageSize),
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
res.json({ success: true, data: { items, total, page: Number(page), pageSize: Number(pageSize) } })
|
||||
res.json({ success: true, data: { items, total, page, pageSize } })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
@@ -79,7 +80,7 @@ router.patch('/:id', authMiddleware, async (req: AuthRequest, res: Response, nex
|
||||
if (existing.status !== 'DRAFT') {
|
||||
return res.status(400).json({ success: false, error: { code: 'NOT_DRAFT', message: '仅草稿状态可编辑' } })
|
||||
}
|
||||
const { title, employeeId, formData, remark } = req.body
|
||||
const { title, employeeId, formData, remark } = updateWorkProcessSchema.parse(req.body)
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: req.params.id },
|
||||
data: {
|
||||
@@ -116,7 +117,7 @@ router.post('/:id/submit', authMiddleware, async (req: AuthRequest, res: Respons
|
||||
}
|
||||
// 生成文书
|
||||
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
|
||||
const doc = generateDocument(process.type, process.formData, org?.name || '')
|
||||
const doc = await generateDocument(process.type, process.formData, org?.name || '')
|
||||
const documents = doc.content ? [doc] : []
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.id },
|
||||
@@ -151,7 +152,7 @@ router.post('/:id/approve', authMiddleware, async (req: AuthRequest, res: Respon
|
||||
return res.status(400).json({ success: false, error: { code: 'EXEC_FAILED', message: `执行失败:${execErr?.message || '未知错误'}` } })
|
||||
}
|
||||
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
|
||||
const doc = generateDocument(process.type, process.formData, org?.name || '')
|
||||
const doc = await generateDocument(process.type, process.formData, org?.name || '')
|
||||
const documents = doc.content ? [doc] : []
|
||||
const updated = await (prisma as any).workProcess.update({
|
||||
where: { id: process.id },
|
||||
@@ -247,7 +248,7 @@ router.get('/:id/preview', authMiddleware, async (req: AuthRequest, res: Respons
|
||||
return res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: '办理记录不存在' } })
|
||||
}
|
||||
const org = await prisma.organization.findUnique({ where: { id: req.user!.orgId } })
|
||||
const doc = generateDocument(process.type, process.formData, org?.name || '')
|
||||
const doc = await generateDocument(process.type, process.formData, org?.name || '')
|
||||
res.json({ success: true, data: doc })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createOrgSchema = z.object({
|
||||
name: z.string().min(2, '企业名称至少2个字').max(50, '企业名称最多50个字'),
|
||||
plan: z.enum(['FREE', 'PRO', 'ENTERPRISE']).default('FREE'),
|
||||
maxEmployees: z.number().int().min(1).max(100000).default(20),
|
||||
city: z.string().max(50).optional(),
|
||||
contactName: z.string().max(30).optional(),
|
||||
contactPhone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
|
||||
adminName: z.string().max(30).optional(),
|
||||
adminPhone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
|
||||
adminPassword: z.string().min(8, '密码至少8位').max(32, '密码最多32位'),
|
||||
})
|
||||
|
||||
export const updateOrgSchema = z.object({
|
||||
name: z.string().min(2).max(50).optional(),
|
||||
plan: z.enum(['FREE', 'PRO', 'ENTERPRISE']).optional(),
|
||||
maxEmployees: z.number().int().min(1).max(100000).optional(),
|
||||
city: z.string().max(50).optional().nullable(),
|
||||
contactName: z.string().max(30).optional().nullable(),
|
||||
contactPhone: z.string().regex(/^1[3-9]\d{9}$/).optional().nullable(),
|
||||
})
|
||||
|
||||
export const updateOrgAdminSchema = z.object({
|
||||
adminName: z.string().max(30).optional(),
|
||||
adminPhone: z.string().regex(/^1[3-9]\d{9}$/).optional(),
|
||||
adminPassword: z.string().min(8, '密码至少8位').max(32).optional(),
|
||||
})
|
||||
|
||||
export const createPlatformAdminSchema = z.object({
|
||||
name: z.string().min(1, '姓名不能为空').max(30),
|
||||
phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确'),
|
||||
password: z.string().min(8, '密码至少8位').max(32, '密码最多32位'),
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createSpecialStatusSchema = z.object({
|
||||
employeeId: z.string().min(1, '员工ID不能为空'),
|
||||
type: z.enum([
|
||||
'PREGNANCY', 'WORK_INJURY', 'MEDICAL_PERIOD', 'OTHER',
|
||||
], { errorMap: () => ({ message: '无效的特殊状态类型' }) }),
|
||||
status: z.enum(['ACTIVE', 'PENDING', 'RESOLVED']).default('ACTIVE'),
|
||||
startDate: z.string().optional().nullable(),
|
||||
endDate: z.string().optional().nullable(),
|
||||
expectedDueDate: z.string().optional().nullable(),
|
||||
injuryDate: z.string().optional().nullable(),
|
||||
injuryDescription: z.string().max(500).optional().nullable(),
|
||||
certificationDate: z.string().optional().nullable(),
|
||||
certificationNo: z.string().max(50).optional().nullable(),
|
||||
disabilityLevel: z.string().max(20).optional().nullable(),
|
||||
assessmentDate: z.string().optional().nullable(),
|
||||
medicalMonths: z.number().int().min(1).max(36).optional().nullable(),
|
||||
description: z.string().max(500).optional().nullable(),
|
||||
attachments: z.array(z.any()).optional().nullable(),
|
||||
reminderDate: z.string().optional().nullable(),
|
||||
})
|
||||
|
||||
export const updateSpecialStatusSchema = z.object({
|
||||
type: z.enum(['PREGNANCY', 'WORK_INJURY', 'MEDICAL_PERIOD', 'OTHER']).optional(),
|
||||
status: z.enum(['ACTIVE', 'PENDING', 'RESOLVED']).optional(),
|
||||
startDate: z.string().optional().nullable(),
|
||||
endDate: z.string().optional().nullable(),
|
||||
expectedDueDate: z.string().optional().nullable(),
|
||||
injuryDate: z.string().optional().nullable(),
|
||||
injuryDescription: z.string().max(500).optional().nullable(),
|
||||
certificationDate: z.string().optional().nullable(),
|
||||
certificationNo: z.string().max(50).optional().nullable(),
|
||||
disabilityLevel: z.string().max(20).optional().nullable(),
|
||||
assessmentDate: z.string().optional().nullable(),
|
||||
medicalMonths: z.number().int().min(1).max(36).optional().nullable(),
|
||||
description: z.string().max(500).optional().nullable(),
|
||||
attachments: z.array(z.any()).optional().nullable(),
|
||||
reminderDate: z.string().optional().nullable(),
|
||||
})
|
||||
@@ -13,3 +13,45 @@ export const terminationQuerySchema = z.object({
|
||||
page: z.coerce.number().min(1).default(1),
|
||||
pageSize: z.coerce.number().min(1).max(50).default(20),
|
||||
})
|
||||
|
||||
export const resignationSchema = z.object({
|
||||
employeeId: z.string().min(1, '员工ID不能为空'),
|
||||
terminationDate: z.string().min(1, '离职日期不能为空'),
|
||||
resignationReason: z.string().max(200).optional(),
|
||||
remark: z.string().max(500).optional(),
|
||||
})
|
||||
|
||||
export const batchTerminatePreviewSchema = z.object({
|
||||
items: z.array(z.object({
|
||||
employeeId: z.string().min(1),
|
||||
reason: z.string().min(1, '解聘原因不能为空'),
|
||||
terminationDate: z.string().min(1, '解聘日期不能为空'),
|
||||
})).min(1, '至少选择一名员工'),
|
||||
})
|
||||
|
||||
export const batchTerminateSchema = z.object({
|
||||
items: z.array(z.object({
|
||||
employeeId: z.string().min(1),
|
||||
reason: z.string().min(1, '解聘原因不能为空'),
|
||||
terminationDate: z.string().min(1, '解聘日期不能为空'),
|
||||
compensation: z.number().min(0).optional(),
|
||||
})).min(1, '至少选择一名员工'),
|
||||
})
|
||||
|
||||
export const createTerminationDraftSchema = z.object({
|
||||
employeeId: z.string().min(1, '员工ID不能为空'),
|
||||
type: z.enum(['TERMINATION', 'RESIGNATION']).optional(),
|
||||
reason: z.string().max(200).optional(),
|
||||
terminationDate: z.string().optional(),
|
||||
compensation: z.number().min(0).optional(),
|
||||
handoverItems: z.array(z.any()).optional(),
|
||||
remark: z.string().max(500).optional(),
|
||||
})
|
||||
|
||||
export const updateTerminationDraftSchema = z.object({
|
||||
reason: z.string().max(200).optional(),
|
||||
terminationDate: z.string().optional(),
|
||||
compensation: z.number().min(0).optional(),
|
||||
handoverItems: z.array(z.any()).optional(),
|
||||
remark: z.string().max(500).optional(),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createWorkProcessSchema = z.object({
|
||||
type: z.enum([
|
||||
'HIRE', 'ONBOARD', 'CUSTOM_CONTRACT', 'INFO_SUBMIT', 'CONFIRM',
|
||||
'CHANGE', 'RENEW', 'SUSPEND', 'INCOME_CERT', 'TERMINATE',
|
||||
'RESCIND', 'LEAVING_CERT', 'FLEXIBLE',
|
||||
]),
|
||||
title: z.string().max(100).optional(),
|
||||
employeeId: z.string().optional().nullable(),
|
||||
formData: z.record(z.any()).optional(),
|
||||
status: z.enum(['DRAFT', 'COMPLETED']).default('DRAFT'),
|
||||
remark: z.string().max(500).optional().nullable(),
|
||||
})
|
||||
|
||||
export const updateWorkProcessSchema = z.object({
|
||||
title: z.string().max(100).optional(),
|
||||
employeeId: z.string().optional().nullable(),
|
||||
formData: z.record(z.any()).optional(),
|
||||
remark: z.string().max(500).optional().nullable(),
|
||||
})
|
||||
@@ -205,6 +205,12 @@ export async function calcBatchEntry(
|
||||
housingOrg = Math.max(0, fullHousingOrg - deductedHousingOrg)
|
||||
}
|
||||
|
||||
// 保存系统计算值(覆盖前)
|
||||
const systemSocialEmp = socialEmp
|
||||
const systemSocialOrg = socialOrg
|
||||
const systemHousingEmp = housingEmp
|
||||
const systemHousingOrg = housingOrg
|
||||
|
||||
// 手动覆盖社保值
|
||||
if (options?.overrideSocial) {
|
||||
if (options.overrideSocial.socialEmp !== undefined) socialEmp = options.overrideSocial.socialEmp
|
||||
@@ -229,9 +235,11 @@ export async function calcBatchEntry(
|
||||
|
||||
// 个税计算
|
||||
let tax = 0
|
||||
let taxBreakdown: any = null
|
||||
if (batchType === 'BONUS') {
|
||||
// 年终奖单独计税
|
||||
tax = calcBonusTax(inputs.bonus)
|
||||
taxBreakdown = { method: '单独计税(年终奖)', bonus: inputs.bonus, tax }
|
||||
} else {
|
||||
// 累计预扣法(补偿金也走累计预扣,但无社保公积金扣除)
|
||||
const year = month.slice(0, 4)
|
||||
@@ -252,8 +260,22 @@ export async function calcBatchEntry(
|
||||
const ytdHousingEmp = archivedEntries.reduce((s, e) => s + e.housingEmp, 0) + housingEmp
|
||||
const ytdSpecialDeduction = employee.specialDeduction * Number(month.slice(5, 7))
|
||||
const ytdTaxDeducted = archivedEntries.reduce((s, e) => s + e.tax, 0)
|
||||
const ytdTaxableIncome = Math.max(0, ytdIncome - 5000 * Number(month.slice(5, 7)) - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction)
|
||||
const deductionAmount = 5000 * Number(month.slice(5, 7))
|
||||
const ytdTaxableIncome = Math.max(0, ytdIncome - deductionAmount - ytdSocialEmp - ytdHousingEmp - ytdSpecialDeduction)
|
||||
tax = calcCumulativeTax(ytdTaxableIncome, ytdTaxDeducted)
|
||||
taxBreakdown = {
|
||||
method: '累计预扣法',
|
||||
month: Number(month.slice(5, 7)),
|
||||
ytdIncome,
|
||||
deductionAmount,
|
||||
ytdSocialEmp,
|
||||
ytdHousingEmp,
|
||||
ytdSpecialDeduction,
|
||||
ytdTaxableIncome,
|
||||
ytdTaxDeducted,
|
||||
currentMonthTax: tax,
|
||||
archivedCount: archivedEntries.length,
|
||||
}
|
||||
}
|
||||
|
||||
const netPay = totalPay - socialEmp - housingEmp - tax
|
||||
@@ -263,7 +285,12 @@ export async function calcBatchEntry(
|
||||
socialOrg: Math.round(socialOrg * 100) / 100,
|
||||
housingEmp: Math.round(housingEmp * 100) / 100,
|
||||
housingOrg: Math.round(housingOrg * 100) / 100,
|
||||
systemSocialEmp: Math.round(systemSocialEmp * 100) / 100,
|
||||
systemSocialOrg: Math.round(systemSocialOrg * 100) / 100,
|
||||
systemHousingEmp: Math.round(systemHousingEmp * 100) / 100,
|
||||
systemHousingOrg: Math.round(systemHousingOrg * 100) / 100,
|
||||
tax,
|
||||
taxBreakdown,
|
||||
totalPay: Math.round(totalPay * 100) / 100,
|
||||
netPay: Math.round(netPay * 100) / 100,
|
||||
}
|
||||
|
||||
@@ -666,7 +666,7 @@ export async function getDashboardData(orgId: string) {
|
||||
const yearEnd = new Date(now.getFullYear(), 11, 31, 23, 59, 59)
|
||||
|
||||
const [
|
||||
employeeCount, highRisks, pendingRisks, riskItems, resolvedItems,
|
||||
employeeCount, _highRisks, _pendingRisks, riskItems, resolvedItems,
|
||||
overtimeRecords, payslips, batchEntries, socialConfig, housingConfig,
|
||||
monthContracts, monthTerminations, monthDisciplinary, monthAttendance,
|
||||
monthSeverancePay,
|
||||
@@ -1217,17 +1217,13 @@ export async function getCostAnalysis(orgId: string, month: string) {
|
||||
const monthNum = parseInt(month.slice(5, 7))
|
||||
|
||||
// 当月数据
|
||||
const currentMonthStart = new Date(year, monthNum - 1, 1)
|
||||
const currentMonthEnd = new Date(year, monthNum, 0, 23, 59, 59)
|
||||
|
||||
// 上月(环比)
|
||||
const prevMonthStart = new Date(year, monthNum - 2, 1)
|
||||
const prevMonthEnd = new Date(year, monthNum - 1, 0, 23, 59, 59)
|
||||
const prevMonthStr = `${prevMonthStart.getFullYear()}-${String(prevMonthStart.getMonth() + 1).padStart(2, '0')}`
|
||||
|
||||
// 去年同月(同比)
|
||||
const lastYearMonthStart = new Date(year - 1, monthNum - 1, 1)
|
||||
const lastYearMonthEnd = new Date(year - 1, monthNum, 0, 23, 59, 59)
|
||||
const lastYearMonthStr = `${lastYearMonthStart.getFullYear()}-${String(lastYearMonthStart.getMonth() + 1).padStart(2, '0')}`
|
||||
|
||||
// 获取各月归档批次汇总(按员工去重,与 getDashboardData 口径一致)
|
||||
@@ -1802,7 +1798,7 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
||||
|
||||
const [
|
||||
risksResolved,
|
||||
lossAvoidedAgg,
|
||||
_lossAvoidedAgg,
|
||||
aiConversations,
|
||||
aiReviews,
|
||||
contractsSigned,
|
||||
@@ -2043,11 +2039,6 @@ export async function getAnnualValueReport(orgId: string, year: number) {
|
||||
// 总价值 = 规避损失 + 节约成本
|
||||
const totalValue = adjustedLossAvoided + costSaved
|
||||
|
||||
// ROI = 总价值 / 系统成本(年费 12000 元),上限 9999%
|
||||
const systemCost = 12000
|
||||
const rawRoi = systemCost > 0 ? Math.round((totalValue / systemCost) * 100) : 0
|
||||
const roi = Math.min(rawRoi, 9999)
|
||||
|
||||
const metrics = {
|
||||
risksResolved,
|
||||
lossAvoided,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import prisma from '../lib/prisma'
|
||||
import { encrypt } from '../lib/crypto'
|
||||
import crypto from 'crypto'
|
||||
import { createDraft as createTerminationDraft, executeTermination, createResignation } from './termination.service'
|
||||
import { createEmployee, addContract, batchRenew } from './contract.service'
|
||||
import { createDraft as createTerminationDraft, executeTermination } from './termination.service'
|
||||
import { createEmployee, addContract } from './contract.service'
|
||||
import { runRiskDetection } from './risk.service'
|
||||
|
||||
// 13类流程定义
|
||||
@@ -67,7 +66,7 @@ export async function executeWorkProcess(processId: string, type: string, formDa
|
||||
return { employeeId }
|
||||
}
|
||||
case 'CONFIRM': {
|
||||
const { employeeId, confirmDate, regularSalary } = formData
|
||||
const { employeeId, regularSalary } = formData
|
||||
if (employeeId) {
|
||||
if (regularSalary) {
|
||||
await prisma.employee.update({
|
||||
@@ -196,7 +195,7 @@ export async function executeWorkProcess(processId: string, type: string, formDa
|
||||
return {}
|
||||
}
|
||||
case 'FLEXIBLE': {
|
||||
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate, payMethod } = formData
|
||||
const { name, phone, idCardNumber, department, agreementStartDate, agreementEndDate } = formData
|
||||
const empResult = await createEmployee(orgId, userId, {
|
||||
name,
|
||||
department: department || '灵活用工',
|
||||
@@ -244,7 +243,23 @@ export async function executeWorkProcess(processId: string, type: string, formDa
|
||||
}
|
||||
|
||||
// 生成文书预览
|
||||
export function generateDocument(type: string, formData: any, orgName: string): { name: string; content: string } {
|
||||
export async function generateDocument(type: string, formData: any, orgName: string): Promise<{ name: string; content: string }> {
|
||||
// 如果指定了企业自定义模板,使用企业模板渲染
|
||||
if (formData.enterpriseTemplateId) {
|
||||
const tpl = await (prisma as any).enterpriseTemplate.findFirst({
|
||||
where: { id: formData.enterpriseTemplateId },
|
||||
})
|
||||
if (tpl) {
|
||||
let content = tpl.content
|
||||
// 替换变量 {{var}}
|
||||
const allVars: Record<string, string> = { ...formData, companyName: orgName }
|
||||
for (const [key, value] of Object.entries(allVars)) {
|
||||
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value ?? ''))
|
||||
}
|
||||
return { name: `${tpl.name}.doc`, content }
|
||||
}
|
||||
}
|
||||
|
||||
const templates: Record<string, (data: any, org: string) => string> = {
|
||||
INCOME_CERT: (data, org) => `收入证明
|
||||
|
||||
|
||||
Reference in New Issue
Block a user