增加组织人员同步基础能力
This commit is contained in:
parent
f842c508b7
commit
fc2e060038
33
README.md
33
README.md
@ -93,6 +93,39 @@ cd /opt/equipment-management
|
||||
|
||||
默认只保留最近 `3` 份备份,备份目录在 `/opt/backups/equipment-management`。详细说明见 `BACKUP.md`。
|
||||
|
||||
## 人员与组织同步
|
||||
|
||||
后端已预留钉钉中台同步能力,支持同步:
|
||||
|
||||
- `hr_groups` -> 部门
|
||||
- `hr_employee` + `hr_employee_register` -> 使用者
|
||||
|
||||
同步接口:
|
||||
|
||||
```bash
|
||||
GET /api/sync/hr/preview
|
||||
POST /api/sync/hr/run
|
||||
```
|
||||
|
||||
这两个接口都只允许管理员访问。源库必须保持只读,不允许对 SQL Server 做写操作。
|
||||
|
||||
建议配置的环境变量:
|
||||
|
||||
```bash
|
||||
SQLSERVER_HOST=218.93.191.16
|
||||
SQLSERVER_PORT=1433
|
||||
SQLSERVER_DATABASE=JSDataMiddlePlatform
|
||||
SQLSERVER_USER=nanting
|
||||
SQLSERVER_PASSWORD=***
|
||||
HR_SYNC_ENABLED=false
|
||||
HR_SYNC_SCHEDULE=03:00
|
||||
HR_SYNC_INACTIVE_STATUS_VALUES=5
|
||||
```
|
||||
|
||||
- `HR_SYNC_ENABLED=true` 时,后端会启动每日定时同步
|
||||
- `HR_SYNC_SCHEDULE` 建议使用 `03:00`
|
||||
- `HR_SYNC_INACTIVE_STATUS_VALUES` 用来标记源系统里的非在职状态码
|
||||
|
||||
## 当前线上资源
|
||||
|
||||
- 前端:`http://1.13.22.133`
|
||||
|
||||
739
backend/package-lock.json
generated
739
backend/package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"express": "^4.18.0",
|
||||
"express-session": "^1.17.3",
|
||||
"keycloak-connect": "^23.0.7",
|
||||
"mssql": "^11.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.0.0"
|
||||
},
|
||||
@ -33,6 +34,268 @@
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure-rest/core-client": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/@azure-rest/core-client/-/core-client-2.5.1.tgz",
|
||||
"integrity": "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.10.0",
|
||||
"@azure/core-rest-pipeline": "^1.22.0",
|
||||
"@azure/core-tracing": "^1.3.0",
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/abort-controller": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz",
|
||||
"integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-auth": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.10.1.tgz",
|
||||
"integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-util": "^1.13.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-client": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.10.1.tgz",
|
||||
"integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.10.0",
|
||||
"@azure/core-rest-pipeline": "^1.22.0",
|
||||
"@azure/core-tracing": "^1.3.0",
|
||||
"@azure/core-util": "^1.13.0",
|
||||
"@azure/logger": "^1.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http-compat": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-http-compat/-/core-http-compat-2.3.2.tgz",
|
||||
"integrity": "sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@azure/core-client": "^1.10.0",
|
||||
"@azure/core-rest-pipeline": "^1.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-lro/-/core-lro-2.7.2.tgz",
|
||||
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-util": "^1.2.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-paging": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-paging/-/core-paging-1.6.2.tgz",
|
||||
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-rest-pipeline": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz",
|
||||
"integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.10.0",
|
||||
"@azure/core-tracing": "^1.3.0",
|
||||
"@azure/core-util": "^1.13.0",
|
||||
"@azure/logger": "^1.3.0",
|
||||
"@typespec/ts-http-runtime": "^0.3.4",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-tracing": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.3.1.tgz",
|
||||
"integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-util": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.13.1.tgz",
|
||||
"integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/identity": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/identity/-/identity-4.13.0.tgz",
|
||||
"integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.9.0",
|
||||
"@azure/core-client": "^1.9.2",
|
||||
"@azure/core-rest-pipeline": "^1.17.0",
|
||||
"@azure/core-tracing": "^1.0.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/logger": "^1.0.0",
|
||||
"@azure/msal-browser": "^4.2.0",
|
||||
"@azure/msal-node": "^3.5.0",
|
||||
"open": "^10.1.0",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/keyvault-common": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
|
||||
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/abort-controller": "^2.0.0",
|
||||
"@azure/core-auth": "^1.3.0",
|
||||
"@azure/core-client": "^1.5.0",
|
||||
"@azure/core-rest-pipeline": "^1.8.0",
|
||||
"@azure/core-tracing": "^1.0.0",
|
||||
"@azure/core-util": "^1.10.0",
|
||||
"@azure/logger": "^1.1.4",
|
||||
"tslib": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/keyvault-keys": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz",
|
||||
"integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure-rest/core-client": "^2.3.3",
|
||||
"@azure/abort-controller": "^2.1.2",
|
||||
"@azure/core-auth": "^1.9.0",
|
||||
"@azure/core-http-compat": "^2.2.0",
|
||||
"@azure/core-lro": "^2.7.2",
|
||||
"@azure/core-paging": "^1.6.2",
|
||||
"@azure/core-rest-pipeline": "^1.19.0",
|
||||
"@azure/core-tracing": "^1.2.0",
|
||||
"@azure/core-util": "^1.11.0",
|
||||
"@azure/keyvault-common": "^2.0.0",
|
||||
"@azure/logger": "^1.1.4",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/logger": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/logger/-/logger-1.3.0.tgz",
|
||||
"integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typespec/ts-http-runtime": "^0.3.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "4.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-4.29.0.tgz",
|
||||
"integrity": "sha512-/f3eHkSNUTl6DLQHm+bKecjBKcRQxbd/XLx8lvSYp8Nl/HRyPuIPOijt9Dt0sH50/SxOwQ62RnFCmFlGK+bR/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "15.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-15.15.0.tgz",
|
||||
"integrity": "sha512-/n+bN0AKlVa+AOcETkJSKj38+bvFs78BaP4rNtv3MJCmPH0YrHiskMRe74OhyZ5DZjGISlFyxqvf9/4QVEi2tw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "3.8.8",
|
||||
"resolved": "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-3.8.8.tgz",
|
||||
"integrity": "sha512-+f1VrJH1iI517t4zgmuhqORja0bL6LDQXfBqkjuMmfTYXTQQnh1EvwwxO3UbKLT05N0obF72SRHFrC1RBDv5Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.15.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
@ -115,6 +378,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-joda/core": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/@js-joda/core/-/core-5.7.0.tgz",
|
||||
"integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.22.0",
|
||||
"resolved": "https://registry.npmmirror.com/@prisma/client/-/client-5.22.0.tgz",
|
||||
@ -183,6 +452,12 @@
|
||||
"@prisma/debug": "5.22.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tediousjs/connection-string": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/@tediousjs/connection-string/-/connection-string-0.5.0.tgz",
|
||||
"integrity": "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testim/chrome-version": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@testim/chrome-version/-/chrome-version-1.1.4.tgz",
|
||||
@ -327,7 +602,6 @@
|
||||
"version": "20.19.25",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz",
|
||||
"integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@ -348,6 +622,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/readable-stream": {
|
||||
"version": "4.0.23",
|
||||
"resolved": "https://registry.npmmirror.com/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz",
|
||||
@ -391,6 +674,32 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typespec/ts-http-runtime": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz",
|
||||
"integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/abort-controller": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-target-shim": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
@ -435,7 +744,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
@ -800,6 +1108,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -823,6 +1137,21 @@
|
||||
"node": ">=0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz",
|
||||
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"run-applescript": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@ -945,6 +1274,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz",
|
||||
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/compare-versions": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/compare-versions/-/compare-versions-6.1.1.tgz",
|
||||
@ -1123,7 +1461,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@ -1144,6 +1481,46 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/default-browser": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz",
|
||||
"integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bundle-name": "^4.1.0",
|
||||
"default-browser-id": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/default-browser-id": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz",
|
||||
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/define-lazy-prop": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
||||
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/degenerator": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/degenerator/-/degenerator-5.0.1.tgz",
|
||||
@ -1242,6 +1619,15 @@
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -1398,6 +1784,24 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-target-shim": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/exceljs": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz",
|
||||
@ -1961,7 +2365,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
@ -1975,7 +2378,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
@ -2088,6 +2490,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz",
|
||||
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -2111,6 +2528,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-inside-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"is-inside-container": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@ -2134,6 +2569,21 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz",
|
||||
"integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-inside-container": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is2": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/is2/-/is2-2.0.9.tgz",
|
||||
@ -2155,6 +2605,34 @@
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-md4": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/js-md4/-/js-md4-0.3.2.tgz",
|
||||
"integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
||||
@ -2167,6 +2645,17 @@
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jwk-to-pem": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/jwk-to-pem/-/jwk-to-pem-2.0.7.tgz",
|
||||
@ -2178,6 +2667,16 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keycloak-connect": {
|
||||
"version": "23.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/keycloak-connect/-/keycloak-connect-23.0.7.tgz",
|
||||
@ -2250,6 +2749,12 @@
|
||||
"integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
@ -2269,24 +2774,48 @@
|
||||
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnil": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz",
|
||||
"integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isundefined": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
|
||||
"integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
@ -2456,6 +2985,26 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mssql": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/mssql/-/mssql-11.0.1.tgz",
|
||||
"integrity": "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tediousjs/connection-string": "^0.5.0",
|
||||
"commander": "^11.0.0",
|
||||
"debug": "^4.3.3",
|
||||
"rfdc": "^1.3.0",
|
||||
"tarn": "^3.0.2",
|
||||
"tedious": "^18.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"mssql": "bin/mssql"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
@ -2523,6 +3072,12 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/native-duplexpair": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz",
|
||||
"integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@ -2631,6 +3186,24 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/open": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz",
|
||||
"integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"default-browser": "^5.2.1",
|
||||
"define-lazy-prop": "^3.0.0",
|
||||
"is-inside-container": "^1.0.0",
|
||||
"wsl-utils": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||
@ -2736,6 +3309,15 @@
|
||||
"fsevents": "2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@ -2912,6 +3494,12 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rfdc": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz",
|
||||
@ -2925,6 +3513,18 @@
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz",
|
||||
"integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@ -2967,7 +3567,6 @@
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -3192,6 +3791,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
@ -3276,6 +3881,15 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/tarn": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz",
|
||||
"integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tcp-port-used": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz",
|
||||
@ -3312,6 +3926,100 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/tedious": {
|
||||
"version": "18.6.2",
|
||||
"resolved": "https://registry.npmmirror.com/tedious/-/tedious-18.6.2.tgz",
|
||||
"integrity": "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/core-auth": "^1.7.2",
|
||||
"@azure/identity": "^4.2.1",
|
||||
"@azure/keyvault-keys": "^4.4.0",
|
||||
"@js-joda/core": "^5.6.1",
|
||||
"@types/node": ">=18",
|
||||
"bl": "^6.0.11",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-md4": "^0.3.2",
|
||||
"native-duplexpair": "^1.0.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tedious/node_modules/bl": {
|
||||
"version": "6.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/bl/-/bl-6.1.6.tgz",
|
||||
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"inherits": "^2.0.4",
|
||||
"readable-stream": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tedious/node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tedious/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tedious/node_modules/readable-stream": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"events": "^3.3.0",
|
||||
"process": "^0.11.10",
|
||||
"string_decoder": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tedious/node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz",
|
||||
@ -3410,8 +4118,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
@ -3470,7 +4177,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
@ -3546,6 +4252,21 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wsl-utils": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
"integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-wsl": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
"express": "^4.18.0",
|
||||
"express-session": "^1.17.3",
|
||||
"keycloak-connect": "^23.0.7",
|
||||
"mssql": "^11.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.0.0"
|
||||
},
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
ALTER TABLE `department`
|
||||
DROP INDEX `department_name_key`,
|
||||
ADD COLUMN `source_group_id` INT NULL,
|
||||
ADD COLUMN `source_parent_group_id` INT NULL,
|
||||
ADD COLUMN `source_system` VARCHAR(30) NULL,
|
||||
ADD COLUMN `last_synced_at` DATETIME(3) NULL;
|
||||
|
||||
CREATE UNIQUE INDEX `department_source_group_id_key`
|
||||
ON `department`(`source_group_id`);
|
||||
|
||||
CREATE INDEX `department_source_system_idx`
|
||||
ON `department`(`source_system`);
|
||||
|
||||
CREATE INDEX `department_last_synced_at_idx`
|
||||
ON `department`(`last_synced_at`);
|
||||
|
||||
ALTER TABLE `person`
|
||||
ADD COLUMN `source_userid` VARCHAR(100) NULL,
|
||||
ADD COLUMN `source_system` VARCHAR(30) NULL,
|
||||
ADD COLUMN `source_main_department_id` INT NULL,
|
||||
ADD COLUMN `source_dept_ids` TEXT NULL,
|
||||
ADD COLUMN `manager_source_userid` VARCHAR(100) NULL,
|
||||
ADD COLUMN `source_employee_status` VARCHAR(30) NULL,
|
||||
ADD COLUMN `last_synced_at` DATETIME(3) NULL;
|
||||
|
||||
CREATE UNIQUE INDEX `person_source_userid_key`
|
||||
ON `person`(`source_userid`);
|
||||
|
||||
CREATE INDEX `person_source_system_idx`
|
||||
ON `person`(`source_system`);
|
||||
|
||||
CREATE INDEX `person_last_synced_at_idx`
|
||||
ON `person`(`last_synced_at`);
|
||||
@ -28,12 +28,16 @@ model AssetType {
|
||||
}
|
||||
|
||||
model Department {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique @db.VarChar(100)
|
||||
status String @default("active") @db.VarChar(20)
|
||||
parent_id Int? // 父部门ID,null表示根部门
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
name String @db.VarChar(100)
|
||||
status String @default("active") @db.VarChar(20)
|
||||
parent_id Int? // 父部门ID,null表示根部门
|
||||
source_group_id Int? @unique
|
||||
source_parent_group_id Int?
|
||||
source_system String? @db.VarChar(30)
|
||||
last_synced_at DateTime?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
// 自关联 - 部门层级
|
||||
parent Department? @relation("DepartmentHierarchy", fields: [parent_id], references: [id])
|
||||
@ -43,6 +47,8 @@ model Department {
|
||||
|
||||
@@index([status])
|
||||
@@index([parent_id])
|
||||
@@index([source_system])
|
||||
@@index([last_synced_at])
|
||||
@@map("department")
|
||||
}
|
||||
|
||||
@ -172,24 +178,33 @@ model SiteConfig {
|
||||
}
|
||||
|
||||
model Person {
|
||||
id Int @id @default(autoincrement())
|
||||
nickname String @db.VarChar(50)
|
||||
real_name String @db.VarChar(50)
|
||||
id_number String? @unique @db.VarChar(50)
|
||||
department String? @db.VarChar(100)
|
||||
position String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(20)
|
||||
email String? @db.VarChar(100)
|
||||
status String @default("active") @db.VarChar(20)
|
||||
remarks String? @db.Text
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
nickname String @db.VarChar(50)
|
||||
real_name String @db.VarChar(50)
|
||||
id_number String? @unique @db.VarChar(50)
|
||||
department String? @db.VarChar(100)
|
||||
position String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(20)
|
||||
email String? @db.VarChar(100)
|
||||
status String @default("active") @db.VarChar(20)
|
||||
remarks String? @db.Text
|
||||
source_userid String? @unique @db.VarChar(100)
|
||||
source_system String? @db.VarChar(30)
|
||||
source_main_department_id Int?
|
||||
source_dept_ids String? @db.Text
|
||||
manager_source_userid String? @db.VarChar(100)
|
||||
source_employee_status String? @db.VarChar(30)
|
||||
last_synced_at DateTime?
|
||||
created_at DateTime @default(now())
|
||||
updated_at DateTime @updatedAt
|
||||
|
||||
borrow_records BorrowRecord[]
|
||||
|
||||
@@index([nickname])
|
||||
@@index([real_name])
|
||||
@@index([status])
|
||||
@@index([source_system])
|
||||
@@index([last_synced_at])
|
||||
@@map("person")
|
||||
}
|
||||
|
||||
|
||||
@ -7,10 +7,64 @@ const baseSelect = {
|
||||
name: true,
|
||||
status: true,
|
||||
parent_id: true,
|
||||
source_group_id: true,
|
||||
source_parent_group_id: true,
|
||||
source_system: true,
|
||||
last_synced_at: true,
|
||||
created_at: true,
|
||||
updated_at: true
|
||||
}
|
||||
|
||||
const buildDepartmentLevelMap = (departments: Array<{ id: number; parent_id: number | null }>) => {
|
||||
const byId = new Map(departments.map((dept) => [dept.id, dept]))
|
||||
const cache = new Map<number, number>()
|
||||
|
||||
const resolveLevel = (id: number): number => {
|
||||
if (cache.has(id)) {
|
||||
return cache.get(id)!
|
||||
}
|
||||
const current = byId.get(id)
|
||||
if (!current || !current.parent_id) {
|
||||
cache.set(id, 0)
|
||||
return 0
|
||||
}
|
||||
const level = resolveLevel(current.parent_id) + 1
|
||||
cache.set(id, level)
|
||||
return level
|
||||
}
|
||||
|
||||
departments.forEach((dept) => resolveLevel(dept.id))
|
||||
return cache
|
||||
}
|
||||
|
||||
const buildDepartmentTree = (
|
||||
departments: Array<{
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
parent_id: number | null
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
_count: { equipments: number; children: number }
|
||||
}>
|
||||
) => {
|
||||
const childrenMap = new Map<number | null, typeof departments>()
|
||||
for (const department of departments) {
|
||||
const siblings = childrenMap.get(department.parent_id) || []
|
||||
siblings.push(department)
|
||||
childrenMap.set(department.parent_id, siblings)
|
||||
}
|
||||
|
||||
const buildNode = (department: typeof departments[number]): any => ({
|
||||
...department,
|
||||
equipment_count: department._count.equipments,
|
||||
children_count: department._count.children,
|
||||
children: (childrenMap.get(department.id) || []).map(buildNode)
|
||||
})
|
||||
|
||||
return (childrenMap.get(null) || []).map(buildNode)
|
||||
}
|
||||
|
||||
// 获取启用的部门列表(下拉选项,含层级信息)
|
||||
export const getDepartmentOptions = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
@ -25,11 +79,11 @@ export const getDepartmentOptions = async (_req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 添加层级信息
|
||||
const levelMap = buildDepartmentLevelMap(departments)
|
||||
const options = departments.map(dept => ({
|
||||
...dept,
|
||||
parent_name: dept.parent?.name || null,
|
||||
level: dept.parent_id ? 1 : 0
|
||||
level: levelMap.get(dept.id) || 0
|
||||
}))
|
||||
|
||||
successResponse(res, options, '获取成功')
|
||||
@ -51,23 +105,7 @@ export const getDepartmentTree = async (_req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 构建树形结构
|
||||
const rootDepts = departments.filter(d => !d.parent_id)
|
||||
const childDepts = departments.filter(d => d.parent_id)
|
||||
|
||||
const tree = rootDepts.map(root => ({
|
||||
...root,
|
||||
equipment_count: root._count.equipments,
|
||||
children_count: root._count.children,
|
||||
children: childDepts
|
||||
.filter(child => child.parent_id === root.id)
|
||||
.map(child => ({
|
||||
...child,
|
||||
equipment_count: child._count.equipments,
|
||||
children_count: child._count.children,
|
||||
children: []
|
||||
}))
|
||||
}))
|
||||
const tree = buildDepartmentTree(departments)
|
||||
|
||||
successResponse(res, tree, '获取成功')
|
||||
} catch (error: any) {
|
||||
@ -109,10 +147,11 @@ export const getDepartmentList = async (req: Request, res: Response) => {
|
||||
prisma.department.count({ where })
|
||||
])
|
||||
|
||||
const levelMap = buildDepartmentLevelMap(list)
|
||||
const formattedList = list.map(dept => ({
|
||||
...dept,
|
||||
parent_name: dept.parent?.name || null,
|
||||
level: dept.parent_id ? 1 : 0,
|
||||
level: levelMap.get(dept.id) || 0,
|
||||
equipment_count: dept._count.equipments,
|
||||
children_count: dept._count.children
|
||||
}))
|
||||
@ -149,16 +188,25 @@ const validateParentDepartment = async (parentId: number | null | undefined, cur
|
||||
return '父部门已禁用,无法设置'
|
||||
}
|
||||
|
||||
// 检查父部门是否已经是子部门(限制两级)
|
||||
if (parent.parent_id) {
|
||||
return '不能将子部门设为父部门(最多支持两级层级)'
|
||||
}
|
||||
|
||||
// 检查是否设置自己为父部门
|
||||
if (currentId && parentId === currentId) {
|
||||
return '不能将部门设为自己的子部门'
|
||||
}
|
||||
|
||||
if (currentId) {
|
||||
let cursor = parent
|
||||
while (cursor.parent_id) {
|
||||
if (cursor.parent_id === currentId) {
|
||||
return '不能将部门设为自己的子部门'
|
||||
}
|
||||
cursor = await prisma.department.findUnique({
|
||||
where: { id: cursor.parent_id },
|
||||
select: { id: true, parent_id: true, status: true }
|
||||
}) as any
|
||||
if (!cursor) break
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@ -226,11 +274,6 @@ export const updateDepartment = async (req: Request, res: Response) => {
|
||||
|
||||
// 如果要设置父部门,验证有效性
|
||||
if (parent_id !== undefined) {
|
||||
// 如果该部门有子部门,不能将其变为子部门
|
||||
if (parent_id && existing._count.children > 0) {
|
||||
return errorResponse(res, '该部门下存在子部门,不能将其设为子部门', 400)
|
||||
}
|
||||
|
||||
const parentError = await validateParentDepartment(parent_id, Number(id))
|
||||
if (parentError) {
|
||||
return errorResponse(res, parentError, 400)
|
||||
|
||||
22
backend/src/controllers/syncController.ts
Normal file
22
backend/src/controllers/syncController.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Request, Response } from 'express'
|
||||
import { errorResponse, successResponse } from '../utils/response'
|
||||
import { previewHrSync, runHrSync } from '../services/hrSyncService'
|
||||
|
||||
export const previewHrSyncController = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await previewHrSync()
|
||||
successResponse(res, result, '预览成功')
|
||||
} catch (error: any) {
|
||||
errorResponse(res, error.message || '预览同步数据失败', 500)
|
||||
}
|
||||
}
|
||||
|
||||
export const runHrSyncController = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const operator = res.locals.user?.username || res.locals.user?.name || 'system-sync'
|
||||
const result = await runHrSync(operator)
|
||||
successResponse(res, result, '同步成功')
|
||||
} catch (error: any) {
|
||||
errorResponse(res, error.message || '执行同步失败', 500)
|
||||
}
|
||||
}
|
||||
@ -23,8 +23,10 @@ import maintenanceRoutes from './routes/maintenance'
|
||||
import batchRoutes from './routes/batch'
|
||||
import exportRoutes from './routes/export'
|
||||
import searchRoutes from './routes/search'
|
||||
import syncRoutes from './routes/sync'
|
||||
import { keycloak, memoryStore } from './utils/keycloak'
|
||||
import { protect, requireAdmin } from './middleware/auth'
|
||||
import { startHrSyncScheduler } from './services/hrSyncService'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
@ -76,6 +78,7 @@ app.use('/api/maintenance', protect(), maintenanceRoutes)
|
||||
app.use('/api/batch', protect(), batchRoutes)
|
||||
app.use('/api/export', protect(), exportRoutes)
|
||||
app.use('/api/search', protect(), searchRoutes)
|
||||
app.use('/api/sync', protect(), requireAdmin(), syncRoutes)
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
@ -95,3 +98,5 @@ app.use((err: any, req: express.Request, res: express.Response, next: express.Ne
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is running on http://localhost:${PORT}`)
|
||||
})
|
||||
|
||||
startHrSyncScheduler()
|
||||
|
||||
9
backend/src/routes/sync.ts
Normal file
9
backend/src/routes/sync.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Router } from 'express'
|
||||
import { previewHrSyncController, runHrSyncController } from '../controllers/syncController'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/hr/preview', previewHrSyncController)
|
||||
router.post('/hr/run', runHrSyncController)
|
||||
|
||||
export default router
|
||||
493
backend/src/services/hrSyncService.ts
Normal file
493
backend/src/services/hrSyncService.ts
Normal file
@ -0,0 +1,493 @@
|
||||
import prisma from '../utils/prisma'
|
||||
import sql from 'mssql'
|
||||
|
||||
type SourceDepartment = {
|
||||
sourceGroupId: number
|
||||
sourceParentGroupId: number | null
|
||||
name: string
|
||||
}
|
||||
|
||||
type SourcePerson = {
|
||||
sourceUserId: string
|
||||
nickname: string
|
||||
realName: string
|
||||
idNumber: string | null
|
||||
departmentName: string | null
|
||||
position: string | null
|
||||
phone: string | null
|
||||
email: string | null
|
||||
sourceMainDepartmentId: number | null
|
||||
sourceDeptIds: string | null
|
||||
managerSourceUserId: string | null
|
||||
sourceEmployeeStatus: string | null
|
||||
status: 'active' | 'inactive'
|
||||
}
|
||||
|
||||
type HrSyncResult = {
|
||||
departments: {
|
||||
total: number
|
||||
created: number
|
||||
updated: number
|
||||
deactivated: number
|
||||
}
|
||||
persons: {
|
||||
total: number
|
||||
created: number
|
||||
updated: number
|
||||
deactivated: number
|
||||
}
|
||||
}
|
||||
|
||||
const SOURCE_SYSTEM = 'ding_hr'
|
||||
let schedulerStarted = false
|
||||
let schedulerRunning = false
|
||||
let lastSchedulerRunKey = ''
|
||||
|
||||
const getSqlServerConfig = (): any => {
|
||||
const server = process.env.SQLSERVER_HOST
|
||||
const port = Number(process.env.SQLSERVER_PORT || '1433')
|
||||
const database = process.env.SQLSERVER_DATABASE
|
||||
const user = process.env.SQLSERVER_USER
|
||||
const password = process.env.SQLSERVER_PASSWORD
|
||||
|
||||
if (!server || !database || !user || !password) {
|
||||
throw new Error('SQL Server 同步配置不完整,请设置 SQLSERVER_HOST / DATABASE / USER / PASSWORD')
|
||||
}
|
||||
|
||||
return {
|
||||
server,
|
||||
port,
|
||||
database,
|
||||
user,
|
||||
password,
|
||||
options: {
|
||||
encrypt: true,
|
||||
trustServerCertificate: true
|
||||
},
|
||||
pool: {
|
||||
max: 5,
|
||||
min: 0,
|
||||
idleTimeoutMillis: 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safeTrim = (value: unknown) => {
|
||||
if (value === null || value === undefined) return null
|
||||
const text = String(value).trim()
|
||||
return text ? text : null
|
||||
}
|
||||
|
||||
const normalizePhone = (value: unknown) => {
|
||||
const text = safeTrim(value)
|
||||
if (!text) return null
|
||||
return text.replace(/^\+86-?/, '')
|
||||
}
|
||||
|
||||
const parseSourceInt = (value: unknown) => {
|
||||
const text = safeTrim(value)
|
||||
if (!text) return null
|
||||
const num = Number(text)
|
||||
return Number.isNaN(num) ? null : num
|
||||
}
|
||||
|
||||
const parseExtensionFlowerName = (extension: unknown) => {
|
||||
const text = safeTrim(extension)
|
||||
if (!text) return null
|
||||
try {
|
||||
const parsed = JSON.parse(text)
|
||||
const value = parsed?.花名 || parsed?.flowerName
|
||||
return safeTrim(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const resolvePersonStatus = (active: unknown, employeeStatus: unknown): 'active' | 'inactive' => {
|
||||
const inactiveStatusValues = new Set(
|
||||
(process.env.HR_SYNC_INACTIVE_STATUS_VALUES || '5')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
const sourceStatus = safeTrim(employeeStatus)
|
||||
const activeFlag =
|
||||
active === true ||
|
||||
active === 1 ||
|
||||
String(active).toLowerCase() === 'true'
|
||||
|
||||
if (!activeFlag) {
|
||||
return 'inactive'
|
||||
}
|
||||
|
||||
if (sourceStatus && inactiveStatusValues.has(sourceStatus)) {
|
||||
return 'inactive'
|
||||
}
|
||||
|
||||
return 'active'
|
||||
}
|
||||
|
||||
const getSourceDepartments = async (pool: any): Promise<SourceDepartment[]> => {
|
||||
const result = await pool
|
||||
.request()
|
||||
.query(`
|
||||
SELECT
|
||||
Id AS sourceGroupId,
|
||||
parent_id AS sourceParentGroupId,
|
||||
name
|
||||
FROM dbo.hr_groups
|
||||
ORDER BY Id
|
||||
`)
|
||||
|
||||
return result.recordset.map((row: any) => ({
|
||||
sourceGroupId: Number(row.sourceGroupId),
|
||||
sourceParentGroupId: row.sourceParentGroupId === null ? null : Number(row.sourceParentGroupId),
|
||||
name: String(row.name).trim()
|
||||
}))
|
||||
}
|
||||
|
||||
const getSourcePersons = async (pool: any): Promise<SourcePerson[]> => {
|
||||
const result = await pool
|
||||
.request()
|
||||
.query(`
|
||||
SELECT
|
||||
e.userid AS sourceUserId,
|
||||
e.name AS employeeName,
|
||||
e.mobile,
|
||||
e.job_number,
|
||||
e.title,
|
||||
e.email,
|
||||
e.dept_id_list,
|
||||
e.manager_userid,
|
||||
e.active,
|
||||
e.extension,
|
||||
r.[sys00-name] AS registerName,
|
||||
r.[sys00-flowerName] AS flowerName,
|
||||
r.[sys02-realName] AS realName,
|
||||
r.[sys02-certNo] AS certNo,
|
||||
r.[sys00-mobile] AS registerMobile,
|
||||
r.[sys00-position] AS registerPosition,
|
||||
r.[sys00-dept] AS registerDept,
|
||||
r.[sys00-mainDeptId] AS mainDeptId,
|
||||
r.[sys00-deptIds] AS registerDeptIds,
|
||||
r.[sys01-employeeStatus] AS employeeStatus
|
||||
FROM dbo.hr_employee e
|
||||
LEFT JOIN dbo.hr_employee_register r
|
||||
ON r.userid = e.userid
|
||||
`)
|
||||
|
||||
return result.recordset
|
||||
.map((row: any) => {
|
||||
const fallbackName = safeTrim(row.employeeName) || safeTrim(row.registerName) || safeTrim(row.sourceUserId)
|
||||
const nickname =
|
||||
safeTrim(row.flowerName) ||
|
||||
parseExtensionFlowerName(row.extension) ||
|
||||
fallbackName ||
|
||||
'未命名'
|
||||
|
||||
const realName = safeTrim(row.realName) || fallbackName || nickname
|
||||
|
||||
return {
|
||||
sourceUserId: String(row.sourceUserId).trim(),
|
||||
nickname,
|
||||
realName,
|
||||
idNumber: safeTrim(row.certNo),
|
||||
departmentName: safeTrim(row.registerDept),
|
||||
position: safeTrim(row.registerPosition) || safeTrim(row.title),
|
||||
phone: normalizePhone(row.registerMobile) || normalizePhone(row.mobile),
|
||||
email: safeTrim(row.email),
|
||||
sourceMainDepartmentId: parseSourceInt(row.mainDeptId),
|
||||
sourceDeptIds: safeTrim(row.registerDeptIds) || safeTrim(row.dept_id_list),
|
||||
managerSourceUserId: safeTrim(row.manager_userid),
|
||||
sourceEmployeeStatus: safeTrim(row.employeeStatus),
|
||||
status: resolvePersonStatus(row.active, row.employeeStatus)
|
||||
} satisfies SourcePerson
|
||||
})
|
||||
.filter((item: SourcePerson) => item.sourceUserId)
|
||||
}
|
||||
|
||||
const syncDepartments = async (sourceDepartments: SourceDepartment[]) => {
|
||||
const existing = await prisma.department.findMany({
|
||||
where: { source_system: SOURCE_SYSTEM },
|
||||
select: {
|
||||
id: true,
|
||||
source_group_id: true
|
||||
}
|
||||
})
|
||||
|
||||
const existingIds = new Set(existing.map((item) => item.source_group_id).filter(Boolean) as number[])
|
||||
let created = 0
|
||||
let updated = 0
|
||||
|
||||
for (const department of sourceDepartments) {
|
||||
const exists = existingIds.has(department.sourceGroupId)
|
||||
|
||||
await prisma.department.upsert({
|
||||
where: { source_group_id: department.sourceGroupId },
|
||||
update: {
|
||||
name: department.name,
|
||||
status: 'active',
|
||||
source_parent_group_id: department.sourceParentGroupId,
|
||||
source_system: SOURCE_SYSTEM,
|
||||
last_synced_at: new Date()
|
||||
},
|
||||
create: {
|
||||
name: department.name,
|
||||
status: 'active',
|
||||
source_group_id: department.sourceGroupId,
|
||||
source_parent_group_id: department.sourceParentGroupId,
|
||||
source_system: SOURCE_SYSTEM,
|
||||
last_synced_at: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
if (exists) {
|
||||
updated++
|
||||
} else {
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
const syncedDepartments = await prisma.department.findMany({
|
||||
where: { source_system: SOURCE_SYSTEM },
|
||||
select: {
|
||||
id: true,
|
||||
source_group_id: true,
|
||||
source_parent_group_id: true
|
||||
}
|
||||
})
|
||||
|
||||
const groupIdToDepartmentId = new Map(
|
||||
syncedDepartments
|
||||
.filter((item) => item.source_group_id !== null)
|
||||
.map((item) => [item.source_group_id as number, item.id])
|
||||
)
|
||||
|
||||
for (const department of syncedDepartments) {
|
||||
const targetParentId = department.source_parent_group_id
|
||||
? groupIdToDepartmentId.get(department.source_parent_group_id) || null
|
||||
: null
|
||||
|
||||
await prisma.department.update({
|
||||
where: { id: department.id },
|
||||
data: { parent_id: targetParentId }
|
||||
})
|
||||
}
|
||||
|
||||
const sourceGroupIds = sourceDepartments.map((item) => item.sourceGroupId)
|
||||
const deactivatedResult = await prisma.department.updateMany({
|
||||
where: {
|
||||
source_system: SOURCE_SYSTEM,
|
||||
source_group_id: {
|
||||
notIn: sourceGroupIds
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: 'inactive',
|
||||
last_synced_at: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: sourceDepartments.length,
|
||||
created,
|
||||
updated,
|
||||
deactivated: deactivatedResult.count
|
||||
}
|
||||
}
|
||||
|
||||
const resolveUniqueIdNumber = async (sourceUserId: string, idNumber: string | null) => {
|
||||
if (!idNumber) return null
|
||||
const conflict = await prisma.person.findFirst({
|
||||
where: {
|
||||
id_number: idNumber,
|
||||
NOT: {
|
||||
source_userid: sourceUserId
|
||||
}
|
||||
},
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
return conflict ? null : idNumber
|
||||
}
|
||||
|
||||
const syncPersons = async (sourcePersons: SourcePerson[]) => {
|
||||
const existing = await prisma.person.findMany({
|
||||
where: { source_system: SOURCE_SYSTEM },
|
||||
select: {
|
||||
id: true,
|
||||
source_userid: true
|
||||
}
|
||||
})
|
||||
|
||||
const existingIds = new Set(existing.map((item) => item.source_userid).filter(Boolean) as string[])
|
||||
let created = 0
|
||||
let updated = 0
|
||||
|
||||
for (const person of sourcePersons) {
|
||||
const normalizedIdNumber = await resolveUniqueIdNumber(person.sourceUserId, person.idNumber)
|
||||
const exists = existingIds.has(person.sourceUserId)
|
||||
|
||||
await prisma.person.upsert({
|
||||
where: { source_userid: person.sourceUserId },
|
||||
update: {
|
||||
nickname: person.nickname,
|
||||
real_name: person.realName,
|
||||
id_number: normalizedIdNumber,
|
||||
department: person.departmentName,
|
||||
position: person.position,
|
||||
phone: person.phone,
|
||||
email: person.email,
|
||||
status: person.status,
|
||||
source_system: SOURCE_SYSTEM,
|
||||
source_main_department_id: person.sourceMainDepartmentId,
|
||||
source_dept_ids: person.sourceDeptIds,
|
||||
manager_source_userid: person.managerSourceUserId,
|
||||
source_employee_status: person.sourceEmployeeStatus,
|
||||
last_synced_at: new Date()
|
||||
},
|
||||
create: {
|
||||
nickname: person.nickname,
|
||||
real_name: person.realName,
|
||||
id_number: normalizedIdNumber,
|
||||
department: person.departmentName,
|
||||
position: person.position,
|
||||
phone: person.phone,
|
||||
email: person.email,
|
||||
status: person.status,
|
||||
source_userid: person.sourceUserId,
|
||||
source_system: SOURCE_SYSTEM,
|
||||
source_main_department_id: person.sourceMainDepartmentId,
|
||||
source_dept_ids: person.sourceDeptIds,
|
||||
manager_source_userid: person.managerSourceUserId,
|
||||
source_employee_status: person.sourceEmployeeStatus,
|
||||
last_synced_at: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
if (exists) {
|
||||
updated++
|
||||
} else {
|
||||
created++
|
||||
}
|
||||
}
|
||||
|
||||
const sourceUserIds = sourcePersons.map((item) => item.sourceUserId)
|
||||
const deactivatedResult = await prisma.person.updateMany({
|
||||
where: {
|
||||
source_system: SOURCE_SYSTEM,
|
||||
source_userid: {
|
||||
notIn: sourceUserIds
|
||||
}
|
||||
},
|
||||
data: {
|
||||
status: 'inactive',
|
||||
last_synced_at: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
total: sourcePersons.length,
|
||||
created,
|
||||
updated,
|
||||
deactivated: deactivatedResult.count
|
||||
}
|
||||
}
|
||||
|
||||
const writeSyncLog = async (result: HrSyncResult, operator = 'system-sync') => {
|
||||
await prisma.operationLog.create({
|
||||
data: {
|
||||
user_id: null,
|
||||
username: operator,
|
||||
operation_type: '同步钉钉组织与人员',
|
||||
operation_details: JSON.stringify(result),
|
||||
ip_address: null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const previewHrSync = async () => {
|
||||
const pool = await sql.connect(getSqlServerConfig())
|
||||
try {
|
||||
const [departments, persons] = await Promise.all([
|
||||
getSourceDepartments(pool),
|
||||
getSourcePersons(pool)
|
||||
])
|
||||
|
||||
return {
|
||||
departments: {
|
||||
total: departments.length,
|
||||
sample: departments.slice(0, 10)
|
||||
},
|
||||
persons: {
|
||||
total: persons.length,
|
||||
sample: persons.slice(0, 10)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await pool.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const runHrSync = async (operator = 'system-sync') => {
|
||||
const pool = await sql.connect(getSqlServerConfig())
|
||||
try {
|
||||
const [departments, persons] = await Promise.all([
|
||||
getSourceDepartments(pool),
|
||||
getSourcePersons(pool)
|
||||
])
|
||||
|
||||
const departmentResult = await syncDepartments(departments)
|
||||
const personResult = await syncPersons(persons)
|
||||
const result = {
|
||||
departments: departmentResult,
|
||||
persons: personResult
|
||||
}
|
||||
|
||||
await writeSyncLog(result, operator)
|
||||
return result
|
||||
} finally {
|
||||
await pool.close()
|
||||
}
|
||||
}
|
||||
|
||||
export const startHrSyncScheduler = () => {
|
||||
if (schedulerStarted) {
|
||||
return
|
||||
}
|
||||
schedulerStarted = true
|
||||
|
||||
const enabled = process.env.HR_SYNC_ENABLED === 'true'
|
||||
if (!enabled) {
|
||||
console.log('[hr-sync] scheduler disabled')
|
||||
return
|
||||
}
|
||||
|
||||
const scheduleTime = process.env.HR_SYNC_SCHEDULE || '03:00'
|
||||
|
||||
setInterval(async () => {
|
||||
const now = new Date()
|
||||
const hh = String(now.getHours()).padStart(2, '0')
|
||||
const mm = String(now.getMinutes()).padStart(2, '0')
|
||||
const currentTime = `${hh}:${mm}`
|
||||
const runKey = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`
|
||||
|
||||
if (currentTime !== scheduleTime || schedulerRunning || lastSchedulerRunKey === runKey) {
|
||||
return
|
||||
}
|
||||
|
||||
schedulerRunning = true
|
||||
try {
|
||||
await runHrSync('system-scheduler')
|
||||
lastSchedulerRunKey = runKey
|
||||
console.log(`[hr-sync] completed at ${now.toISOString()}`)
|
||||
} catch (error) {
|
||||
console.error('[hr-sync] failed:', error)
|
||||
} finally {
|
||||
schedulerRunning = false
|
||||
}
|
||||
}, 60 * 1000)
|
||||
|
||||
console.log(`[hr-sync] scheduler started, schedule=${scheduleTime}`)
|
||||
}
|
||||
1
backend/src/types/mssql.d.ts
vendored
Normal file
1
backend/src/types/mssql.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'mssql';
|
||||
Loading…
x
Reference in New Issue
Block a user