更新项目管理模块中合同管理、项目安装、项目验收、回访记录等功能

This commit is contained in:
wk committed 2026-09-30 17:04:55 +08:00
1 parent 307e9cf1b9
commit b3cef22a54
32 files changed
+6094 -2600

No files matched your search

+648 -3
View File
@@ -8,11 +8,14 @@
"name": "smart-grid-iot-server",
"version": "1.0.0",
"dependencies": {
"@aws-sdk/client-s3": "^3.1131.0",
"@aws-sdk/s3-request-presigner": "^3.1131.0",
"@midwayjs/cache-manager": "^3.19.0",
"@midwayjs/core": "^3.19.0",
"@midwayjs/koa": "^3.19.0",
"@midwayjs/logger": "^4.0.1",
"@midwayjs/sequelize": "^3.19.0",
"@midwayjs/upload": "^3.20.0",
"@midwayjs/validate": "^3.19.0",
"@types/validator": "^13.15.10",
"bcryptjs": "^2.4.3",
@@ -48,6 +51,331 @@
"node": ">=18"
}
},
"node_modules/@aws-sdk/checksums": {
"version": "3.1001.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz",
"integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/client-s3": {
"version": "3.1141.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1141.0.tgz",
"integrity": "sha512-uOVH37xGLenAdJkCPCin/JJG2PgWrFcSsDnQ9+C9Zq8N9Oalo5ol4xmn5fG28iWAlA/b/9boQZgHbMh+UsIhcg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/checksums": "^3.1001.1",
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/credential-provider-node": "^3.972.84",
"@aws-sdk/middleware-sdk-s3": "^3.972.77",
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/fetch-http-handler": "^5.8.0",
"@smithy/node-http-handler": "^4.12.1",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.978.1",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz",
"integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.6",
"@aws-sdk/xml-builder": "^3.972.41",
"@aws/lambda-invoke-store": "^0.3.0",
"@smithy/core": "^3.35.0",
"@smithy/signature-v4": "^5.7.3",
"@smithy/types": "^4.19.0",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.72",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz",
"integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-http": {
"version": "3.972.74",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz",
"integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/fetch-http-handler": "^5.8.0",
"@smithy/node-http-handler": "^4.12.1",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.973.17",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz",
"integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/credential-provider-env": "^3.972.72",
"@aws-sdk/credential-provider-http": "^3.972.74",
"@aws-sdk/credential-provider-login": "^3.972.79",
"@aws-sdk/credential-provider-process": "^3.972.72",
"@aws-sdk/credential-provider-sso": "^3.973.16",
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
"@aws-sdk/nested-clients": "^3.997.46",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/credential-provider-imds": "^4.5.2",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-login": {
"version": "3.972.79",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz",
"integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/nested-clients": "^3.997.46",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-node": {
"version": "3.972.84",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz",
"integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.72",
"@aws-sdk/credential-provider-http": "^3.972.74",
"@aws-sdk/credential-provider-ini": "^3.973.17",
"@aws-sdk/credential-provider-process": "^3.972.72",
"@aws-sdk/credential-provider-sso": "^3.973.16",
"@aws-sdk/credential-provider-web-identity": "^3.972.78",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/credential-provider-imds": "^4.5.2",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-process": {
"version": "3.972.72",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz",
"integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.973.16",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz",
"integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/nested-clients": "^3.997.46",
"@aws-sdk/token-providers": "3.1138.0",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.972.78",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz",
"integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/nested-clients": "^3.997.46",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/middleware-sdk-s3": {
"version": "3.972.77",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz",
"integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/nested-clients": {
"version": "3.997.46",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz",
"integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/fetch-http-handler": "^5.8.0",
"@smithy/node-http-handler": "^4.12.1",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/s3-request-presigner": {
"version": "3.1141.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1141.0.tgz",
"integrity": "sha512-HHHcP3gvLqeY9GPptpxLvA6VwDu+MWKCoaGGm0UtSUBh1hC7xJ9nNHoybgFfstC4lrj+k8oD6a1rWbGBWKYdag==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/signature-v4-multi-region": "^3.996.47",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/signature-v4-multi-region": {
"version": "3.996.47",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz",
"integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.974.6",
"@smithy/signature-v4": "^5.7.3",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/token-providers": {
"version": "3.1138.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz",
"integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.978.1",
"@aws-sdk/nested-clients": "^3.997.46",
"@aws-sdk/types": "^3.974.6",
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/types": {
"version": "3.974.6",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz",
"integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/xml-builder": {
"version": "3.972.41",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz",
"integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws/lambda-invoke-store": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.7",
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -1623,6 +1951,19 @@
"node": ">=12"
}
},
"node_modules/@midwayjs/upload": {
"version": "3.20.24",
"resolved": "https://registry.npmjs.org/@midwayjs/upload/-/upload-3.20.24.tgz",
"integrity": "sha512-DOjmQ7sCHvzkyebZjWAEq6fp7LvyQXsfNp3zQs5CA8a2d64OQpJbgso6iXeZVB2OwkLV592Wuq0UfHXXhvgXsA==",
"license": "MIT",
"dependencies": {
"file-type": "16.5.4",
"raw-body": "2.5.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@midwayjs/validate": {
"version": "3.20.24",
"resolved": "https://registry.npmmirror.com/@midwayjs/validate/-/validate-3.20.24.tgz",
@@ -1752,6 +2093,93 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@smithy/core": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz",
"integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/credential-provider-imds": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
"integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.33.2",
"@smithy/types": "^4.17.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/fetch-http-handler": {
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
"integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.33.3",
"@smithy/types": "^4.18.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/node-http-handler": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
"integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.33.3",
"@smithy/types": "^4.18.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/signature-v4": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz",
"integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.35.0",
"@smithy/types": "^4.19.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/types": {
"version": "4.19.0",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz",
"integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": {
"version": "1.0.12",
"resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -2490,6 +2918,18 @@
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/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",
@@ -2795,6 +3235,26 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"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"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.32",
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz",
@@ -2827,6 +3287,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.15",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -2907,6 +3373,30 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/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/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",
@@ -3835,6 +4325,24 @@
"node": ">=4"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/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.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
@@ -3936,6 +4444,23 @@
"bser": "2.1.1"
}
},
"node_modules/file-type": {
"version": "16.5.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
"integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
"license": "MIT",
"dependencies": {
"readable-web-to-node-stream": "^3.0.0",
"strtok3": "^6.2.4",
"token-types": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/filelist": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.6.tgz",
@@ -4390,6 +4915,26 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"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": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
@@ -6323,6 +6868,19 @@
"node": ">=8"
}
},
"node_modules/peek-readable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
"integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/pg": {
"version": "8.23.0",
"resolved": "https://registry.npmmirror.com/pg/-/pg-8.23.0.tgz",
@@ -6533,6 +7091,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz",
@@ -6614,7 +7181,6 @@
"version": "2.5.2",
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
@@ -6630,7 +7196,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"depd": "2.0.0",
@@ -6647,7 +7212,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -6660,6 +7224,38 @@
"dev": true,
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/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/readable-web-to-node-stream": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
"integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
"license": "MIT",
"dependencies": {
"readable-stream": "^4.7.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
@@ -7190,6 +7786,15 @@
"node": ">= 0.6"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/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/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz",
@@ -7265,6 +7870,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strtok3": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
"integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"peek-readable": "^4.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/superagent": {
"version": "8.1.2",
"resolved": "https://registry.npmmirror.com/superagent/-/superagent-8.1.2.tgz",
@@ -7371,6 +7993,23 @@
"node": ">=0.6"
}
},
"node_modules/token-types": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
"integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/toposort-class": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/toposort-class/-/toposort-class-1.0.1.tgz",
@@ -7507,6 +8146,12 @@
"json5": "lib/cli.js"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsscmp": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/tsscmp/-/tsscmp-1.0.6.tgz",
@@ -0,0 +1,16 @@
import { Controller, Get, Post, Put, Del, Inject, Query, Body, Param } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { InstallOrderService } from "../service/install_order.service";
@Controller("/api/install-orders")
export class InstallOrderController {
@Inject() ctx!: Context;
@Inject() installOrderService!: InstallOrderService;
@Get("/") async list(@Query() query: any) { return this.installOrderService.list(query, this.ctx); }
@Get("/:id") async detail(@Param("id") id: string) { return this.installOrderService.detail(Number(id), this.ctx); }
@Post("/") async create(@Body() body: any) { return this.installOrderService.create(body, this.ctx); }
@Put("/:id") async update(@Param("id") id: string, @Body() body: any) { return this.installOrderService.update(Number(id), body, this.ctx); }
@Put("/:id/complete") async complete(@Param("id") id: string) { return this.installOrderService.complete(Number(id), this.ctx); }
@Del("/:id") async delete(@Param("id") id: string) { return this.installOrderService.delete(Number(id), this.ctx); }
}
+51
View File
@@ -0,0 +1,51 @@
import {
IsNotEmpty,
IsOptional,
IsString,
IsInt,
IsArray,
IsDateString,
IsIn,
ArrayNotEmpty,
Matches,
Min,
Max,
MaxLength,
ArrayMaxSize,
ArrayMinSize
} from "class-validator";
export class InstallOrderQueryDTO {
@IsOptional() @IsInt() @Min(1) page?: number = 1;
@IsOptional() @IsInt() @Min(1) @Max(200) pageSize?: number = 10;
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
// 派单日期范围 [startDate, endDate],YYYY-MM-DD
@IsOptional() @IsArray() @ArrayMinSize(2) @ArrayMaxSize(2) @IsDateString({}, { each: true }) dateRange?: string[];
}
export class InstallOrderCreateDTO {
@IsNotEmpty({ message: "所属客户不能为空" }) @IsInt() customerId!: number;
@IsNotEmpty({ message: "关联方案不能为空" }) @IsInt() planId!: number;
@IsNotEmpty({ message: "安装人不能为空" }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer!: string;
@IsNotEmpty({ message: "派单日期不能为空" }) @IsDateString() dispatchDate!: string;
@IsOptional() @IsInt() workerId?: number;
@IsOptional() @IsDateString() startDate?: string;
@IsOptional() @IsDateString() endDate?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
}
export class InstallOrderUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsInt() planId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) installer?: string;
@IsOptional() @IsDateString() dispatchDate?: string;
@IsOptional() @IsInt() workerId?: number;
@IsOptional() @IsDateString() startDate?: string;
@IsOptional() @IsDateString() endDate?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) remark?: string;
@IsOptional() @IsInt() @IsIn([0, 1]) status?: number;
}
+66
View File
@@ -0,0 +1,66 @@
import { Column, Model, Table, DataType, CreatedAt, ForeignKey, BelongsTo } from "sequelize-typescript";
import Customer from "./customer.entity";
import Worker from "./worker.entity";
import ProjectPlan from "./project_plan.entity";
@Table({
tableName: "install_order",
comment: "安装派单信息表",
timestamps: false,
indexes: [
{ name: "idx_install_customer", fields: ["customer_id"] },
{ name: "idx_install_worker", fields: ["worker_id"] },
{ name: "idx_install_plan", fields: ["plan_id"] },
{ name: "idx_install_status", fields: ["status"] },
{ name: "idx_install_dispatch_date", fields: ["dispatch_date"] }
]
})
export default class InstallOrder extends Model {
@Column({ type: DataType.BIGINT, autoIncrement: true, primaryKey: true })
id!: number;
@Column({ type: DataType.STRING(64), allowNull: false, unique: true, field: "install_no", comment: "安装单号(唯一)" })
installNo!: string;
@ForeignKey(() => Customer)
@Column({ type: DataType.BIGINT, allowNull: false, field: "customer_id", comment: "客户ID" })
customerId!: number;
@Column({ type: DataType.STRING(64), comment: "安装人" })
installer!: string;
@ForeignKey(() => Worker)
@Column({ type: DataType.BIGINT, field: "worker_id", comment: "共享电工ID" })
workerId!: number;
@Column({ type: DataType.DATEONLY, field: "dispatch_date", comment: "派单日期" })
dispatchDate!: string;
@Column({ type: DataType.DATEONLY, field: "start_date", comment: "开始日期" })
startDate!: string;
@Column({ type: DataType.DATEONLY, field: "end_date", comment: "结束日期" })
endDate!: string;
@ForeignKey(() => ProjectPlan)
@Column({ type: DataType.BIGINT, allowNull: false, field: "plan_id", comment: "方案ID" })
planId!: number;
@Column({ type: DataType.TEXT, comment: "备注" })
remark!: string;
@Column({ type: DataType.SMALLINT, allowNull: false, defaultValue: 0, comment: "状态:0已派发 1已完成" })
status!: number;
@BelongsTo(() => Customer, { foreignKey: "customerId", as: "customer" })
customer!: Customer;
@BelongsTo(() => Worker, { foreignKey: "workerId", as: "worker" })
worker!: Worker;
@BelongsTo(() => ProjectPlan, { foreignKey: "planId", as: "plan" })
plan!: ProjectPlan;
@CreatedAt
created_at!: Date;
}
+1
View File
@@ -23,6 +23,7 @@ const PERMISSION_MAP: Record<string, string> = {
'/api/survey-orders': 'project:survey',
'/api/project-plans': 'project:plan',
'/api/contracts': 'project:contract',
'/api/install-orders': 'project:install',
'/api/acceptances': 'project:acceptance',
'/api/visit-records': 'project:visit',
};
+19 -2
View File
@@ -1,4 +1,4 @@
import { Provide } from "@midwayjs/core";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
@@ -7,6 +7,7 @@ import Acceptance from "../entity/acceptance.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { LogService } from "./log.service";
@Provide()
export class AcceptanceService {
@@ -22,6 +23,9 @@ export class AcceptanceService {
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -57,9 +61,18 @@ export class AcceptanceService {
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ acceptor: { [Op.like]: `%${query.keyword}%` } }, { acceptOpinion: { [Op.like]: `%${query.keyword}%` } }];
where[Op.or] = [
{ acceptNo: { [Op.like]: `%${query.keyword}%` } },
{ acceptor: { [Op.like]: `%${query.keyword}%` } },
{ acceptOpinion: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.acceptStartDate || query.acceptEndDate) {
where.acceptDate = {};
if (query.acceptStartDate) where.acceptDate[Op.gte] = query.acceptStartDate;
if (query.acceptEndDate) where.acceptDate[Op.lte] = query.acceptEndDate;
}
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
@@ -88,6 +101,7 @@ export class AcceptanceService {
try {
data.acceptNo = await this.generateAcceptNo();
const created = await this.acceptModel.create(data);
await this.logService.logAction(ctx, "项目验收", "创建", data.acceptNo, `创建项目验收 ${data.acceptNo}`);
return { code: 200, message: "创建项目验收成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
@@ -108,6 +122,7 @@ export class AcceptanceService {
}
delete data.acceptNo;
await record.update(data);
await this.logService.logAction(ctx, "项目验收", "更新", record.acceptNo, `更新项目验收 ${record.acceptNo}`);
return { code: 200, message: "更新项目验收成功", data: record.toJSON() };
}
@@ -115,7 +130,9 @@ export class AcceptanceService {
const record = await this.acceptModel.findByPk(id);
if (!record) return { code: 404, message: "项目验收不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该验收", data: null };
const acceptNo = record.acceptNo;
await record.destroy();
await this.logService.logAction(ctx, "项目验收", "删除", acceptNo, `删除项目验收 ${acceptNo}`);
return { code: 200, message: "删除项目验收成功", data: null };
}
}
+19 -2
View File
@@ -1,4 +1,4 @@
import { Provide } from "@midwayjs/core";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
@@ -7,6 +7,7 @@ import Contract from "../entity/contract.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { LogService } from "./log.service";
@Provide()
export class ContractService {
@@ -22,6 +23,9 @@ export class ContractService {
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -72,11 +76,20 @@ export class ContractService {
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ xtContractNo: { [Op.like]: `%${query.keyword}%` } }, { contractDesc: { [Op.like]: `%${query.keyword}%` } }];
where[Op.or] = [
{ contractNo: { [Op.like]: `%${query.keyword}%` } },
{ xtContractNo: { [Op.like]: `%${query.keyword}%` } },
{ contractDesc: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.salesmanId) where.salesmanId = query.salesmanId;
if (query.status !== undefined) where.status = query.status;
if (query.signStartDate || query.signEndDate) {
where.signDate = {};
if (query.signStartDate) where.signDate[Op.gte] = query.signStartDate;
if (query.signEndDate) where.signDate[Op.lte] = query.signEndDate;
}
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
@@ -109,6 +122,7 @@ export class ContractService {
data.contractNo = await this.generateContractNo();
if (!data.xtContractNo) data.xtContractNo = await this.generateXtContractNo();
const created = await this.contractModel.create(data);
await this.logService.logAction(ctx, "项目合同", "创建", data.contractNo, `创建项目合同 ${data.contractNo}`);
return { code: 200, message: "创建项目合同成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
@@ -131,6 +145,7 @@ export class ContractService {
}
delete data.contractNo;
await record.update(data);
await this.logService.logAction(ctx, "项目合同", "更新", record.contractNo, `更新项目合同 ${record.contractNo}`);
return { code: 200, message: "更新项目合同成功", data: record.toJSON() };
}
@@ -138,7 +153,9 @@ export class ContractService {
const record = await this.contractModel.findByPk(id);
if (!record) return { code: 404, message: "项目合同不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该合同", data: null };
const contractNo = record.contractNo;
await record.destroy();
await this.logService.logAction(ctx, "项目合同", "删除", contractNo, `删除项目合同 ${contractNo}`);
return { code: 200, message: "删除项目合同成功", data: null };
}
+279
View File
@@ -0,0 +1,279 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
import { Context } from "@midwayjs/koa";
import InstallOrder from "../entity/install_order.entity";
import Customer from "../entity/customer.entity";
import Worker from "../entity/worker.entity";
import ProjectPlan from "../entity/project_plan.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { LogService } from "./log.service";
// 安装派单状态:0已派发 1已完成
const STATUS = { DISPATCHED: 0, COMPLETED: 1 };
@Provide()
export class InstallOrderService {
@InjectRepository(InstallOrder)
installModel!: Repository<InstallOrder>;
@InjectRepository(Customer)
customerModel!: Repository<Customer>;
@InjectRepository(Worker)
workerModel!: Repository<Worker>;
@InjectRepository(ProjectPlan)
planModel!: Repository<ProjectPlan>;
@InjectRepository(SysUserRole)
userRoleModel!: Repository<SysUserRole>;
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
}
private async isCustomerOwner(userId: number, customerId: number): Promise<boolean> {
if (await this.isAdmin(userId)) return true;
const customer = await this.customerModel.findByPk(customerId);
return !!customer && customer.salesmanId === userId;
}
// 工单可见性:管理员 / 客户归属业务员
private async isOrderVisible(userId: number, order: InstallOrder): Promise<boolean> {
if (await this.isAdmin(userId)) return true;
return this.isCustomerOwner(userId, order.customerId);
}
// 单号规则:WH-INST-YYYY-NNNN(年内累计序号),冲突重试 1 次
private async generateInstallNo(retry: number = 0): Promise<string> {
const now = new Date();
const d = new Date(now.getTime() + 8 * 60 * 60 * 1000);
const year = d.getUTCFullYear();
const prefix = `WH-INST-${year}-`;
const last = await this.installModel.findOne({ where: { installNo: { [Op.like]: `${prefix}%` } }, order: [["install_no", "DESC"]] });
let seq = 1;
if (last) seq = (parseInt(last.installNo.slice(-4), 10) || 0) + 1;
const result = `${prefix}${String(seq).padStart(4, "0")}`;
const exists = await this.installModel.findOne({ where: { installNo: result } });
if (exists) {
if (retry < 1) return this.generateInstallNo(retry + 1);
throw new Error("安装单号生成冲突,请重试");
}
return result;
}
// query 参数 → Sequelize where(关键字/客户/状态/派单日期范围)
private buildWhere(query: any): any {
const where: any = {};
if (query.keyword) {
where[Op.or] = [
{ installNo: { [Op.like]: `%${query.keyword}%` } },
{ installer: { [Op.like]: `%${query.keyword}%` } },
{ remark: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined && query.customerId !== "") where.customerId = Number(query.customerId);
if (query.status !== undefined && query.status !== "") where.status = Number(query.status);
// dateRange: [startDate, endDate],作用于 dispatch_date
const range = Array.isArray(query.dateRange) ? query.dateRange : (typeof query.dateRange === "string" ? query.dateRange.split(",") : null);
if (range && range.length === 2 && (range[0] || range[1])) {
const cond: any = {};
if (range[0]) cond[Op.gte] = range[0];
if (range[1]) cond[Op.lte] = range[1];
where.dispatchDate = cond;
}
return where;
}
// 行级隔离:非管理员仅可见「客户归属自己」的工单
private async scopeWhere(baseWhere: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
const where: any = { ...baseWhere };
if (await this.isAdmin(userId)) return where;
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
const ids = myCustomers.map((c) => c.id);
where.customerId = ids.length ? { [Op.in]: ids } : -1;
return where;
}
// 批量补充客户/电工/方案信息,避免 N+1
private async decorate(rows: any[]): Promise<any[]> {
if (rows.length === 0) return rows;
const customerIds = [...new Set(rows.map((r) => r.customerId).filter(Boolean))];
const workerIds = [...new Set(rows.map((r) => r.workerId).filter(Boolean))];
const planIds = [...new Set(rows.map((r) => r.planId).filter(Boolean))];
const [customers, workers, plans] = await Promise.all([
customerIds.length
? this.customerModel.findAll({
where: { id: { [Op.in]: customerIds } },
attributes: ["id", "customerNo", "customerName", "contactPerson", "contactPhone", "address"]
})
: Promise.resolve([]),
workerIds.length
? this.workerModel.findAll({
where: { id: { [Op.in]: workerIds } },
attributes: ["id", "workerName", "phone", "workerTitle"]
})
: Promise.resolve([]),
planIds.length
? this.planModel.findAll({
where: { id: { [Op.in]: planIds } },
attributes: ["id", "planNo", "planDesc", "status"]
})
: Promise.resolve([])
]);
const customerMap = new Map(customers.map((c) => [c.id, c]));
const workerMap = new Map(workers.map((w) => [w.id, w]));
const planMap = new Map(plans.map((p) => [p.id, p]));
for (const row of rows) {
const customer: any = customerMap.get(row.customerId);
const worker: any = workerMap.get(row.workerId);
const plan: any = planMap.get(row.planId);
row.customer = customer ? customer.toJSON() : null;
row.customerName = customer?.customerName || "";
row.worker = worker ? worker.toJSON() : null;
row.workerName = worker?.workerName || "";
row.plan = plan ? plan.toJSON() : null;
row.planNo = plan?.planNo || "";
}
return rows;
}
// 统一清洗入参:空串转 null
private normalizePayload(data: any): any {
const payload: any = { ...data };
for (const key of Object.keys(payload)) {
if (payload[key] === "") payload[key] = null;
}
return payload;
}
async list(query: any, ctx: Context): Promise<any> {
const page = Number(query.page) || 1;
const pageSize = Number(query.pageSize) || 10;
const where = await this.scopeWhere(this.buildWhere(query), ctx);
const { count, rows } = await this.installModel.findAndCountAll({
where,
limit: pageSize,
offset: (page - 1) * pageSize,
order: [["created_at", "DESC"]],
distinct: true
});
const list = await this.decorate(rows.map((r) => r.toJSON()));
return { code: 200, message: "获取安装派单列表成功", data: { list, total: count, page, pageSize } };
}
async detail(id: number, ctx: Context): Promise<any> {
const record = await this.installModel.findByPk(id);
if (!record) return { code: 404, message: "安装派单不存在", data: null };
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权限访问该安装派单", data: null };
const vo: any = record.toJSON();
await this.decorate([vo]);
return { code: 200, message: "获取安装派单详情成功", data: vo };
}
async create(data: any, ctx: Context): Promise<any> {
const userId = ctx.state.user.userId;
if (!(await this.isCustomerOwner(userId, data.customerId))) return { code: 403, message: "无权限为该客户创建安装派单", data: null };
// 方案与客户一致性校验,防止串客户派单
const plan = await this.planModel.findByPk(Number(data.planId));
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
if (Number(plan.customerId) !== Number(data.customerId)) return { code: 400, message: "方案与客户不匹配", data: null };
if (data.workerId) {
const worker = await this.workerModel.findByPk(Number(data.workerId));
if (!worker) return { code: 400, message: "共享电工不存在", data: null };
}
const payload = this.normalizePayload(data);
payload.installNo = await this.generateInstallNo();
if (payload.status === undefined || payload.status === null) payload.status = STATUS.DISPATCHED;
let created: InstallOrder;
try {
created = await this.installModel.create(payload);
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
payload.installNo = await this.generateInstallNo();
created = await this.installModel.create(payload);
} else {
throw err;
}
}
const vo: any = created.toJSON();
await this.decorate([vo]);
await this.logService.logAction(ctx, "安装派单", "创建", created.installNo, `创建安装派单 ${created.installNo}`);
return { code: 200, message: "创建安装派单成功", data: vo };
}
async update(id: number, data: any, ctx: Context): Promise<any> {
const record = await this.installModel.findByPk(id);
if (!record) return { code: 404, message: "安装派单不存在", data: null };
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权操作该安装派单", data: null };
if (record.status === STATUS.COMPLETED) return { code: 400, message: "已完成的派单不可修改", data: null };
delete data.installNo;
if (data.customerId !== undefined && Number(data.customerId) !== Number(record.customerId)) {
if (!(await this.isCustomerOwner(ctx.state.user.userId, Number(data.customerId)))) {
return { code: 403, message: "无权限转移到该客户", data: null };
}
}
if (data.planId !== undefined) {
const plan = await this.planModel.findByPk(Number(data.planId));
if (!plan) return { code: 400, message: "关联方案不存在", data: null };
const targetCustomerId = data.customerId !== undefined ? Number(data.customerId) : Number(record.customerId);
if (Number(plan.customerId) !== targetCustomerId) return { code: 400, message: "方案与客户不匹配", data: null };
}
const payload = this.normalizePayload(data);
await record.update(payload);
const fresh = await this.installModel.findByPk(id);
const vo: any = fresh!.toJSON();
await this.decorate([vo]);
await this.logService.logAction(ctx, "安装派单", "更新", record.installNo, `更新安装派单 ${record.installNo}`);
return { code: 200, message: "更新安装派单成功", data: vo };
}
// 完成派单:仅允许 0已派发 → 1已完成
async complete(id: number, ctx: Context): Promise<any> {
const record = await this.installModel.findByPk(id);
if (!record) return { code: 404, message: "安装派单不存在", data: null };
if (!(await this.isOrderVisible(ctx.state.user.userId, record))) return { code: 403, message: "无权操作该安装派单", data: null };
if (record.status !== STATUS.DISPATCHED) return { code: 400, message: "仅已派发状态的派单可标记完成", data: null };
await record.update({ status: STATUS.COMPLETED });
const vo: any = record.toJSON();
await this.decorate([vo]);
await this.logService.logAction(ctx, "安装派单", "完成", record.installNo, `完成安装派单 ${record.installNo}`);
return { code: 200, message: "安装派单已完成", data: vo };
}
async delete(id: number, ctx: Context): Promise<any> {
const record = await this.installModel.findByPk(id);
if (!record) return { code: 404, message: "安装派单不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该安装派单", data: null };
const installNo = record.installNo;
await record.destroy();
await this.logService.logAction(ctx, "安装派单", "删除", installNo, `删除安装派单 ${installNo}`);
return { code: 200, message: "删除安装派单成功", data: null };
}
}
+19
View File
@@ -1,5 +1,6 @@
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Context } from "@midwayjs/koa";
import SysOperationLog from "../entity/sysOperationLog.entity";
import { Op } from "sequelize";
import dayjs from "dayjs";
@@ -9,6 +10,24 @@ export class LogService {
@InjectRepository(SysOperationLog)
logModel!: typeof SysOperationLog;
// 写入操作日志:操作人userId/模块/动作/目标单号/摘要
// method 与 ip 从 ctx 提取,targetNo+摘要 落 params(JSON) 字段
async logAction(ctx: Context, module: string, action: string, targetNo: string, summary: string): Promise<void> {
try {
const userId = (ctx.state as any)?.user?.userId ?? 0;
await this.logModel.create({
userId,
module,
action,
method: ctx.method || "",
params: JSON.stringify({ targetNo: targetNo || "", summary: summary || "" }),
ip: ctx.ip || ""
} as any);
} catch {
// 日志写入失败不影响主业务流程
}
}
async list(query: {
page?: number;
pageSize?: number;
+19 -2
View File
@@ -1,4 +1,4 @@
import { Provide } from "@midwayjs/core";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
@@ -8,6 +8,7 @@ import ProjectPlanDetail from "../entity/project_plan_detail.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { LogService } from "./log.service";
@Provide()
export class ProjectPlanService {
@@ -26,6 +27,9 @@ export class ProjectPlanService {
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -61,10 +65,19 @@ export class ProjectPlanService {
const where: any = {};
if (query.keyword) {
where[Op.or] = [{ registrar: { [Op.like]: `%${query.keyword}%` } }, { planDesc: { [Op.like]: `%${query.keyword}%` } }];
where[Op.or] = [
{ planNo: { [Op.like]: `%${query.keyword}%` } },
{ registrar: { [Op.like]: `%${query.keyword}%` } },
{ planDesc: { [Op.like]: `%${query.keyword}%` } }
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.status !== undefined) where.status = query.status;
if (query.registerStartDate || query.registerEndDate) {
where.registerTime = {};
if (query.registerStartDate) where.registerTime[Op.gte] = query.registerStartDate;
if (query.registerEndDate) where.registerTime[Op.lte] = query.registerEndDate;
}
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
@@ -99,6 +112,7 @@ export class ProjectPlanService {
await this.detailModel.bulkCreate(details.map((d: any) => ({ ...d, planId: created.id, customerId: created.customerId })));
}
const result = await this.planModel.findByPk(created.id, { include: [this.detailModel] });
await this.logService.logAction(ctx, "项目方案", "创建", data.planNo, `创建项目方案 ${data.planNo}`);
return { code: 200, message: "创建项目方案成功", data: result?.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
@@ -133,6 +147,7 @@ export class ProjectPlanService {
}
}
const result = await this.planModel.findByPk(id, { include: [this.detailModel] });
await this.logService.logAction(ctx, "项目方案", "更新", record.planNo, `更新项目方案 ${record.planNo}`);
return { code: 200, message: "更新项目方案成功", data: result?.toJSON() };
}
@@ -140,8 +155,10 @@ export class ProjectPlanService {
const record = await this.planModel.findByPk(id);
if (!record) return { code: 404, message: "项目方案不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该方案", data: null };
const planNo = record.planNo;
await this.detailModel.destroy({ where: { planId: id } });
await record.destroy();
await this.logService.logAction(ctx, "项目方案", "删除", planNo, `删除项目方案 ${planNo}`);
return { code: 200, message: "删除项目方案成功", data: null };
}
@@ -13,6 +13,7 @@ import SysUser from "../entity/sysUser.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { UploadService } from "./upload.service";
import { LogService } from "./log.service";
// 勘察工单状态:0待指派 1现场勘察中 2待技术复核 3已归档 4预留 5已取消
const STATUS = { PENDING_ASSIGN: 0, SURVEYING: 1, PENDING_REVIEW: 2, ARCHIVED: 3, RESERVED: 4, CANCELED: 5 };
@@ -57,6 +58,9 @@ export class SurveyOrderService {
@Inject()
uploadService!: UploadService;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -479,6 +483,7 @@ export class SurveyOrderService {
await this.createSopResults(created.id, templateCode);
const vo = this.toVO(created);
await this.decorate([vo]);
await this.logService.logAction(ctx, "勘察派单", "创建", created.surveyNo, `创建勘察派单 ${created.surveyNo}`);
return { code: 200, message: "创建勘察派单成功", data: vo };
}
@@ -501,6 +506,7 @@ export class SurveyOrderService {
const fresh = await this.surveyModel.findByPk(id);
const vo = this.toVO(fresh!);
await this.decorate([vo]);
await this.logService.logAction(ctx, "勘察派单", "更新", record.surveyNo, `更新勘察派单 ${record.surveyNo}`);
return { code: 200, message: "更新勘察派单成功", data: vo };
}
@@ -535,6 +541,7 @@ export class SurveyOrderService {
const result = await this.assignCore(id, worker, dto.estimatedFee);
if (!result.ok) return { code: result.code, message: result.message, data: null };
await this.logService.logAction(ctx, "勘察派单", "指派", record.surveyNo, `指派勘察派单 ${record.surveyNo} 至 ${worker.workerName}`);
return { code: 200, message: "派单成功", data: result.data };
}
@@ -555,6 +562,7 @@ export class SurveyOrderService {
failIds.push(id);
}
}
await this.logService.logAction(ctx, "勘察派单", "批量指派", "", `批量指派 ${successCount} 条勘察派单至 ${worker.workerName},失败 ${failIds.length} 条`);
return {
code: 200,
message: `批量派单完成,成功 ${successCount} 条`,
@@ -580,6 +588,7 @@ export class SurveyOrderService {
await record.update(patch);
const vo = this.toVO(record);
await this.decorate([vo]);
await this.logService.logAction(ctx, "勘察派单", "复核", record.surveyNo, `复核勘察派单 ${record.surveyNo}: ${dto.result === "pass" ? "通过归档" : "退回勘察"}`);
return { code: 200, message: dto.result === "pass" ? "复核通过,工单已归档" : "已退回现场勘察中", data: vo };
}
@@ -594,6 +603,7 @@ export class SurveyOrderService {
const urgeCount = (record.urgeCount || 0) + 1;
const lastUrgedAt = new Date();
await record.update({ urgeCount, lastUrgedAt });
await this.logService.logAction(ctx, "勘察派单", "催办", record.surveyNo, `催办勘察派单 ${record.surveyNo}(第 ${urgeCount} 次)`);
return { code: 200, message: "催办成功", data: { urgeCount, lastUrgedAt } };
}
@@ -606,6 +616,7 @@ export class SurveyOrderService {
}
await record.update({ status: STATUS.CANCELED, cancelReason: dto.reason || null });
await this.logService.logAction(ctx, "勘察派单", "取消", record.surveyNo, `取消勘察派单 ${record.surveyNo}`);
return { code: 200, message: "取消工单成功", data: this.toVO(record) };
}
@@ -613,9 +624,11 @@ export class SurveyOrderService {
const record = await this.surveyModel.findByPk(id);
if (!record) return { code: 404, message: "勘察派单不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该勘察派单", data: null };
const surveyNo = record.surveyNo;
// 级联清理回执后删除工单
await this.sopResultModel.destroy({ where: { surveyOrderId: id } });
await record.destroy();
await this.logService.logAction(ctx, "勘察派单", "删除", surveyNo, `删除勘察派单 ${surveyNo}`);
return { code: 200, message: "删除勘察派单成功", data: null };
}
}
+14 -1
View File
@@ -1,4 +1,4 @@
import { Provide } from "@midwayjs/core";
import { Provide, Inject } from "@midwayjs/core";
import { InjectRepository } from "@midwayjs/sequelize";
import { Repository } from "sequelize-typescript";
import { Op, UniqueConstraintError } from "sequelize";
@@ -7,6 +7,7 @@ import VisitRecord from "../entity/visit_record.entity";
import Customer from "../entity/customer.entity";
import SysUserRole from "../entity/sysUserRole.entity";
import SysRole from "../entity/sysRole.entity";
import { LogService } from "./log.service";
@Provide()
export class VisitRecordService {
@@ -22,6 +23,9 @@ export class VisitRecordService {
@InjectRepository(SysRole)
roleModel!: Repository<SysRole>;
@Inject()
logService!: LogService;
private async isAdmin(userId: number): Promise<boolean> {
const userRoles = await this.userRoleModel.findAll({ where: { userId }, include: [this.roleModel] });
return userRoles.some((ur) => (ur as any).role?.roleCode === "ADMIN");
@@ -65,6 +69,11 @@ export class VisitRecordService {
];
}
if (query.customerId !== undefined) where.customerId = query.customerId;
if (query.visitStartDate || query.visitEndDate) {
where.visitTime = {};
if (query.visitStartDate) where.visitTime[Op.gte] = query.visitStartDate;
if (query.visitEndDate) where.visitTime[Op.lte] = query.visitEndDate;
}
if (!(await this.isAdmin(userId))) {
const myCustomers = await this.customerModel.findAll({ where: { salesmanId: userId }, attributes: ["id"] });
@@ -93,6 +102,7 @@ export class VisitRecordService {
try {
data.visitNo = await this.generateVisitNo();
const created = await this.visitModel.create(data);
await this.logService.logAction(ctx, "客户回访", "创建", data.visitNo, `创建客户回访 ${data.visitNo}`);
return { code: 200, message: "创建客户回访成功", data: created.toJSON() };
} catch (err: any) {
if (err instanceof UniqueConstraintError) {
@@ -113,6 +123,7 @@ export class VisitRecordService {
}
delete data.visitNo;
await record.update(data);
await this.logService.logAction(ctx, "客户回访", "更新", record.visitNo, `更新客户回访 ${record.visitNo}`);
return { code: 200, message: "更新客户回访成功", data: record.toJSON() };
}
@@ -120,7 +131,9 @@ export class VisitRecordService {
const record = await this.visitModel.findByPk(id);
if (!record) return { code: 404, message: "客户回访不存在", data: null };
if (!(await this.isCustomerOwner(ctx.state.user.userId, record.customerId))) return { code: 403, message: "无权限删除该回访记录", data: null };
const visitNo = record.visitNo;
await record.destroy();
await this.logService.logAction(ctx, "客户回访", "删除", visitNo, `删除客户回访 ${visitNo}`);
return { code: 200, message: "删除客户回访成功", data: null };
}
}
+79 -1421
View File
File diff suppressed because it is too large. Load diff