增加组织人员同步基础能力

This commit is contained in:
WuYongJie 2026-03-10 21:04:30 +08:00
parent f842c508b7
commit fc2e060038
11 changed files with 1433 additions and 57 deletions

View File

@ -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`

View File

@ -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",

View File

@ -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"
},

View File

@ -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`);

View File

@ -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")
}

View File

@ -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)

View 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)
}
}

View File

@ -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()

View 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

View 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
View File

@ -0,0 +1 @@
declare module 'mssql';