提交 6fcb7409 authored 作者: 王鹏飞's avatar 王鹏飞

feat: add case synchronization feature and integrate OSS for JSON storage

- Added `ali-oss` dependency for OSS integration. - Implemented `import-cases.js` script to read and normalize case data from a JSON source. - Enhanced configuration to include OSS settings for case JSON storage. - Created `oss.js` library for handling OSS uploads. - Updated error handling middleware to improve status code validation. - Refactored authentication service to cache user data and reduce redundant requests. - Introduced `syncCasesToPublic` function in the cases service to upload case data to OSS. - Added new route for case synchronization in the DMS routes. - Updated project service to ensure consistent product name handling.
上级 2bcd3587
......@@ -9,13 +9,19 @@ MYSQL_USER=root
MYSQL_PASSWORD=
MYSQL_DATABASE=com_dms
OSS_REGION=oss-cn-beijing
OSS_BUCKET=webapp-pub
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ENDPOINT=
CASE_JSON_OBJECT_KEY=case_library/case.json
# DMS unified authentication and authorization
# Only set DMS_AUTH_DISABLED=true for local development without SSO.
DMS_AUTH_DISABLED=false
SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info
USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info
USER_CENTER_SALT=
PERMISSION_API_URL=
PERMISSION_APP_ID=
PERMISSION_APP_SECRET=
SSO_USER_CACHE_TTL_SECONDS=180
PERMISSION_CACHE_TTL_SECONDS=180
......@@ -9,14 +9,19 @@ MYSQL_USER=u_com_dms
MYSQL_PASSWORD=com_dms2026
MYSQL_DATABASE=com_dms
OSS_REGION=oss-cn-beijing
OSS_BUCKET=webapp-pub
OSS_ACCESS_KEY_ID=LTAI5t6yKc2m47ZUZTBbUBuP
OSS_ACCESS_KEY_SECRET=mhuPVC43NAkzBaheHyWGjREiIrLax0
OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com
CASE_JSON_OBJECT_KEY=case_library/case.json
# DMS unified authentication and authorization
# Only set DMS_AUTH_DISABLED=true for local development without SSO.
DMS_AUTH_DISABLED=false
SSO_USER_INFO_URL=https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info
USER_CENTER_BATCH_URL=https://api-usercenter.ezijing.com/v2/server/user/multi-get-user-info
USER_CENTER_SALT=n7xAav2tSjn9Ysnr
PERMISSION_API_URL=https://permissions-api.ezijing.com
PERMISSION_APP_ID=ezijing_82b452761ada1b79fd12b9fcbd73629e
PERMISSION_APP_SECRET=f19d1e788ac9ea0e3c72ef2e3caee8f0
SSO_USER_CACHE_TTL_SECONDS=180
PERMISSION_CACHE_TTL_SECONDS=180
......@@ -9,6 +9,7 @@
"version": "2.0.0",
"license": "ISC",
"dependencies": {
"ali-oss": "^6.23.0",
"axios": "^1.15.2",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
......@@ -1243,6 +1244,27 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/address": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz",
"integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/agentkeepalive": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.3.tgz",
"integrity": "sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
......@@ -1260,6 +1282,66 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ali-oss": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/ali-oss/-/ali-oss-6.23.0.tgz",
"integrity": "sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==",
"license": "MIT",
"dependencies": {
"address": "^1.2.2",
"agentkeepalive": "^3.4.1",
"bowser": "^1.6.0",
"copy-to": "^2.0.1",
"dateformat": "^2.0.0",
"debug": "^4.3.4",
"destroy": "^1.0.4",
"end-or-error": "^1.0.1",
"get-ready": "^1.0.0",
"humanize-ms": "^1.2.0",
"is-type-of": "^1.4.0",
"js-base64": "^2.5.2",
"jstoxml": "^2.0.0",
"lodash": "^4.17.21",
"merge-descriptors": "^1.0.1",
"mime": "^2.4.5",
"platform": "^1.3.1",
"pump": "^3.0.0",
"qs": "^6.4.0",
"sdk-base": "^2.0.1",
"stream-http": "2.8.2",
"stream-wormhole": "^1.0.4",
"urllib": "^2.44.0",
"utility": "^1.18.0",
"xml2js": "^0.6.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ali-oss/node_modules/dateformat": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
"integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ali-oss/node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
......@@ -1360,6 +1442,12 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/bowser": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz",
"integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==",
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
......@@ -1386,6 +1474,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
"license": "MIT"
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
......@@ -1498,6 +1592,18 @@
"node": ">=6.6.0"
}
},
"node_modules/copy-to": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz",
"integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
......@@ -1583,6 +1689,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/default-user-agent": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/default-user-agent/-/default-user-agent-1.0.0.tgz",
"integrity": "sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==",
"license": "MIT",
"dependencies": {
"os-name": "~1.0.3"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
......@@ -1609,6 +1727,16 @@
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/detect-europe-js": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz",
......@@ -1629,6 +1757,15 @@
],
"license": "MIT"
},
"node_modules/digest-header": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/digest-header/-/digest-header-1.1.0.tgz",
"integrity": "sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==",
"license": "MIT",
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
......@@ -1815,12 +1952,20 @@
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dev": true,
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/end-or-error": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/end-or-error/-/end-or-error-1.0.1.tgz",
"integrity": "sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==",
"license": "MIT",
"engines": {
"node": ">= 0.11.14"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
......@@ -2322,6 +2467,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
"license": "MIT",
"dependencies": {
"is-extendable": "^0.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-copy": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.3.tgz",
......@@ -2449,6 +2606,18 @@
}
}
},
"node_modules/formstream": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/formstream/-/formstream-1.5.2.tgz",
"integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==",
"license": "MIT",
"dependencies": {
"destroy": "^1.0.4",
"mime": "^2.5.2",
"node-hex": "^1.0.1",
"pause-stream": "~0.0.11"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
......@@ -2546,6 +2715,12 @@
"node": ">= 0.4"
}
},
"node_modules/get-ready": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/get-ready/-/get-ready-1.0.0.tgz",
"integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==",
"license": "MIT"
},
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
......@@ -2663,6 +2838,15 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
......@@ -2730,6 +2914,21 @@
"node": ">= 0.10"
}
},
"node_modules/is-class-hotfix": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz",
"integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==",
"license": "MIT"
},
"node_modules/is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
......@@ -2785,12 +2984,35 @@
],
"license": "MIT"
},
"node_modules/is-type-of": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/is-type-of/-/is-type-of-1.4.0.tgz",
"integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==",
"license": "MIT",
"dependencies": {
"core-util-is": "^1.0.2",
"is-class-hotfix": "~0.0.6",
"isstream": "~0.1.2"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT"
},
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
......@@ -2801,6 +3023,12 @@
"node": ">=10"
}
},
"node_modules/js-base64": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz",
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"license": "BSD-3-Clause"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
......@@ -2834,6 +3062,12 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"node_modules/jstoxml": {
"version": "2.2.9",
"resolved": "https://registry.npmjs.org/jstoxml/-/jstoxml-2.2.9.tgz",
"integrity": "sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==",
"license": "MIT"
},
"node_modules/kareem": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
......@@ -2883,6 +3117,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
......@@ -2947,6 +3187,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
......@@ -2985,12 +3237,23 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/mongodb": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
......@@ -3118,6 +3381,17 @@
"@types/node": ">= 8"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
"object-assign": "^4.0.1",
"thenify-all": "^1.0.0"
}
},
"node_modules/named-placeholders": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
......@@ -3145,6 +3419,15 @@
"node": ">= 0.6"
}
},
"node_modules/node-hex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/node-hex/-/node-hex-1.0.1.tgz",
"integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
......@@ -3213,6 +3496,37 @@
"node": ">= 0.8.0"
}
},
"node_modules/os-name": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/os-name/-/os-name-1.0.3.tgz",
"integrity": "sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==",
"license": "MIT",
"dependencies": {
"osx-release": "^1.0.0",
"win-release": "^1.0.0"
},
"bin": {
"os-name": "cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/osx-release": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/osx-release/-/osx-release-1.1.0.tgz",
"integrity": "sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==",
"license": "MIT",
"dependencies": {
"minimist": "^1.1.0"
},
"bin": {
"osx-release": "cli.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
......@@ -3296,6 +3610,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/pause-stream": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
"integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
"license": [
"MIT",
"Apache2"
],
"dependencies": {
"through": "~2.3"
}
},
"node_modules/pino": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
......@@ -3383,6 +3709,12 @@
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT"
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
......@@ -3393,6 +3725,12 @@
"node": ">= 0.8.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",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
......@@ -3435,7 +3773,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
......@@ -3496,6 +3833,21 @@
"node": ">= 0.10"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
......@@ -3541,6 +3893,12 @@
"node": ">= 18"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
......@@ -3555,6 +3913,24 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sax": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/sdk-base": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sdk-base/-/sdk-base-2.0.1.tgz",
"integrity": "sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==",
"license": "MIT",
"dependencies": {
"get-ready": "~1.0.0"
}
},
"node_modules/secure-json-parse": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
......@@ -3572,6 +3948,15 @@
],
"license": "BSD-3-Clause"
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
......@@ -3819,6 +4204,37 @@
"node": ">= 0.8"
}
},
"node_modules/stream-http": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.2.tgz",
"integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==",
"license": "MIT",
"dependencies": {
"builtin-status-codes": "^3.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.3.6",
"to-arraybuffer": "^1.0.0",
"xtend": "^4.0.0"
}
},
"node_modules/stream-wormhole": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz",
"integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
......@@ -3832,6 +4248,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/thread-stream": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
......@@ -3850,6 +4287,18 @@
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
"license": "MIT"
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
"integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==",
"license": "MIT"
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
......@@ -4502,6 +4951,18 @@
"license": "MIT",
"peer": true
},
"node_modules/unescape": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unescape/-/unescape-1.0.1.tgz",
"integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==",
"license": "MIT",
"dependencies": {
"extend-shallow": "^2.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
......@@ -4521,6 +4982,80 @@
"punycode": "^2.1.0"
}
},
"node_modules/urllib": {
"version": "2.44.1",
"resolved": "https://registry.npmjs.org/urllib/-/urllib-2.44.1.tgz",
"integrity": "sha512-vreOVvFizoiIz5NK9IYMgUknkriHHBVccn2VFfJhgKz6O2qwm0SgjFk4OpXFRDXpdrTx8EzM1DB0/pejrqXwPA==",
"license": "MIT",
"dependencies": {
"any-promise": "^1.3.0",
"content-type": "^1.0.2",
"default-user-agent": "^1.0.0",
"digest-header": "^1.0.0",
"ee-first": "~1.1.1",
"formstream": "^1.1.0",
"humanize-ms": "^1.2.0",
"iconv-lite": "^0.6.3",
"pump": "^3.0.0",
"qs": "^6.4.0",
"statuses": "^1.3.1",
"utility": "^1.16.1"
},
"engines": {
"node": ">= 0.10.0"
},
"peerDependencies": {
"proxy-agent": "^5.0.0"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
}
}
},
"node_modules/urllib/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/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/urllib/node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utility": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/utility/-/utility-1.18.0.tgz",
"integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==",
"license": "MIT",
"dependencies": {
"copy-to": "^2.0.1",
"escape-html": "^1.0.3",
"mkdirp": "^0.5.1",
"mz": "^2.7.0",
"unescape": "^1.0.1"
},
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
......@@ -4566,6 +5101,18 @@
"node": ">= 8"
}
},
"node_modules/win-release": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz",
"integrity": "sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==",
"license": "MIT",
"dependencies": {
"semver": "^5.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
......@@ -4582,6 +5129,37 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
......
......@@ -25,6 +25,7 @@
"pino-pretty": "^13.1.3"
},
"dependencies": {
"ali-oss": "^6.23.0",
"axios": "^1.15.2",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
......
import fs from 'node:fs/promises'
import path from 'node:path'
import process from 'node:process'
import dotenv from 'dotenv'
import mysql from 'mysql2/promise'
dotenv.config({ path: process.env.DOTENV_CONFIG_PATH || '.env' })
const DEFAULT_SOURCE = 'https://webapp-pub.ezijing.com/case_library/case.json'
const args = process.argv.slice(2)
const source = args.find((arg) => arg !== '--dry-run') || process.env.CASE_JSON_SOURCE || DEFAULT_SOURCE
const dryRun = process.env.DRY_RUN === 'true' || args.includes('--dry-run')
const fileTypeNames = {
'1': '案例原文',
'2': '案例PPT',
'3': '案例指导书',
'4': '案例操作视频',
'5': '案例数据集',
'6': '其他资料',
}
async function readSource(sourcePath) {
if (/^https?:\/\//i.test(sourcePath)) {
const response = await fetch(sourcePath)
if (!response.ok) throw new Error(`读取 case.json 失败:HTTP ${response.status}`)
return response.json()
}
const content = await fs.readFile(path.resolve(sourcePath), 'utf8')
return JSON.parse(content)
}
function unwrapCases(payload) {
if (Array.isArray(payload)) return payload
if (Array.isArray(payload?.data)) return payload.data
if (Array.isArray(payload?.list)) return payload.list
if (Array.isArray(payload?.data?.list)) return payload.data.list
throw new Error('case.json 不是案例数组格式')
}
function normalizeText(value, fallback = '') {
return value == null ? fallback : String(value)
}
function normalizeFiles(files, index) {
if (files == null) return []
if (!Array.isArray(files)) throw new Error(`第 ${index + 1} 条案例的 files 不是数组`)
return files.map((file, fileIndex) => {
if (!file || typeof file !== 'object') {
throw new Error(`第 ${index + 1} 条案例的第 ${fileIndex + 1} 个附件格式错误`)
}
const normalized = { ...file }
if (normalized.type != null) normalized.type = String(normalized.type)
if (normalized.type && !normalized.type_name) normalized.type_name = fileTypeNames[normalized.type] || ''
if (normalized.name != null) normalized.name = String(normalized.name)
if (normalized.url != null) normalized.url = String(normalized.url)
return normalized
})
}
function toMysqlDate(value, fallback) {
const text = normalizeText(value).trim()
if (!text) return fallback
// 已经是数据库时间时原样保留,避免时区转换。
const plain = text.match(/^(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2})/)
if (plain) return plain[1].replace('T', ' ')
const date = new Date(text)
if (Number.isNaN(date.getTime())) throw new Error(`无法解析时间:${text}`)
const pad = (part) => String(part).padStart(2, '0')
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}`
}
function normalizeCase(item, index) {
if (!item || typeof item !== 'object') throw new Error(`第 ${index + 1} 条案例格式错误`)
const now = toMysqlDate(new Date().toISOString())
const operator = item.operator || {}
const operatorUserId = normalizeText(item.operatorUserId ?? operator.id)
const operatorName = normalizeText(item.operatorName ?? operator.name, '数据迁移')
const name = normalizeText(item.name, '未命名案例').trim()
if (!name) throw new Error(`第 ${index + 1} 条案例名称为空`)
if (name.length > 255) throw new Error(`第 ${index + 1} 条案例名称超过 255 个字符`)
if (operatorUserId.length > 64) throw new Error(`第 ${index + 1} 条案例操作人 ID 超过 64 个字符`)
if (operatorName.length > 120) throw new Error(`第 ${index + 1} 条案例操作人姓名超过 120 个字符`)
return {
name,
description: normalizeText(item.description),
productId: 2,
productName: '办公智能与应用实践教学平台',
files: JSON.stringify(normalizeFiles(item.files, index)),
operatorUserId,
operatorName,
createdAt: toMysqlDate(item.created_at ?? item.createdAt, now),
updatedAt: toMysqlDate(item.updated_at ?? item.updatedAt, now),
}
}
function createPool() {
if (process.env.DATABASE_URL) return mysql.createPool(process.env.DATABASE_URL)
return mysql.createPool({
host: process.env.MYSQL_HOST || '127.0.0.1',
port: Number(process.env.MYSQL_PORT || 3306),
user: process.env.MYSQL_USER || 'root',
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'com_dms',
})
}
async function main() {
console.log(`读取案例数据:${source}`)
const cases = unwrapCases(await readSource(source)).map(normalizeCase)
console.log(`发现 ${cases.length} 条案例`)
if (dryRun) {
console.log('DRY_RUN=true,仅校验数据,不写入数据库')
return
}
const pool = createPool()
const connection = await pool.getConnection()
try {
const [rows] = await connection.query('SELECT COUNT(*) AS total FROM case_list')
const existingCount = Number(rows[0]?.total || 0)
if (existingCount > 0) {
throw new Error(`case_list 已有 ${existingCount} 条数据,为避免覆盖,已停止导入`)
}
await connection.beginTransaction()
for (const [index, item] of cases.entries()) {
const [result] = await connection.execute(
`INSERT INTO case_list
(name, description, product_id, product_name, files, operator_user_id, operator_name, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
item.name,
item.description,
item.productId,
item.productName,
item.files,
item.operatorUserId,
item.operatorName,
item.createdAt,
item.updatedAt,
],
)
console.log(`[${index + 1}/${cases.length}] → ${result.insertId} ${item.name}`)
}
const [verifyRows] = await connection.query('SELECT COUNT(*) AS total FROM case_list')
const importedCount = Number(verifyRows[0]?.total || 0)
if (importedCount !== cases.length) {
throw new Error(`导入数量校验失败:期望 ${cases.length},实际 ${importedCount}`)
}
await connection.commit()
console.log(`导入完成:${importedCount} 条`)
} catch (error) {
await connection.rollback()
throw error
} finally {
connection.release()
await pool.end()
}
}
main().catch((error) => {
console.error(`导入失败:${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
})
......@@ -16,8 +16,15 @@ const config = {
password: process.env.MYSQL_PASSWORD || '',
database: process.env.MYSQL_DATABASE || 'com_dms',
},
oss: {
region: process.env.OSS_REGION || 'oss-cn-beijing',
bucket: process.env.OSS_BUCKET || 'webapp-pub',
accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
endpoint: process.env.OSS_ENDPOINT || '',
caseJsonObjectKey: process.env.CASE_JSON_OBJECT_KEY || 'case_library/case.json',
},
dmsAuth: {
disabled: process.env.DMS_AUTH_DISABLED === 'true',
ssoUserInfoUrl:
process.env.SSO_USER_INFO_URL || 'https://api-usercenter.ezijing.com/v2/frontend/user/get-user-info',
userCenterBatchUrl:
......@@ -26,6 +33,7 @@ const config = {
permissionApiUrl: process.env.PERMISSION_API_URL || '',
permissionAppId: process.env.PERMISSION_APP_ID || '',
permissionAppSecret: process.env.PERMISSION_APP_SECRET || '',
userInfoCacheTtlSeconds: parseInt(process.env.SSO_USER_CACHE_TTL_SECONDS || '180', 10) || 180,
permissionCacheTtlSeconds: parseInt(process.env.PERMISSION_CACHE_TTL_SECONDS || '180', 10) || 180,
},
wechat: {
......
import OSS from 'ali-oss'
import config from '../config.js'
let client
const getOssClient = () => {
if (client) return client
const { region, bucket, accessKeyId, accessKeySecret, endpoint } = config.oss
if (!accessKeyId || !accessKeySecret) {
throw new Error('OSS 同步配置不完整,请配置 OSS_ACCESS_KEY_ID 和 OSS_ACCESS_KEY_SECRET')
}
client = new OSS({
region,
bucket,
accessKeyId,
accessKeySecret,
...(endpoint ? { endpoint } : {}),
})
return client
}
export const uploadJson = async (objectKey, payload) => {
const content = Buffer.from(JSON.stringify(payload, null, 2), 'utf8')
const result = await getOssClient().put(objectKey, content, {
mime: 'application/json',
headers: { 'Cache-Control': 'no-cache' },
})
return {
objectKey,
url: result.url,
size: content.length,
}
}
......@@ -6,7 +6,8 @@ export const notFound = (req, res) => {
}
export const errorHandler = (err, req, res, _next) => {
const statusCode = err.statusCode || err.status || 500
const candidate = Number(err.statusCode ?? err.status)
const statusCode = Number.isInteger(candidate) && candidate >= 100 && candidate < 1000 ? candidate : 500
const message = process.env.NODE_ENV === 'production' && statusCode === 500 ? 'Internal Server Error' : err.message
logger.error({ err, url: req.url, method: req.method }, 'Request error')
......
......@@ -4,6 +4,8 @@ import { getUserAuthorization } from './permission.client.js'
import { hasAnyDmsRouteAccess, normalizeRoute } from './route-access.js'
const roleTag = (role) => role?.tag
const userCache = new Map()
const userRequests = new Map()
export const getTgc = (req) => {
const cookies = req.headers.cookie || ''
......@@ -22,18 +24,7 @@ const normalizeSsoUser = (payload) => {
}
}
export const authenticateRequest = async (req) => {
if (config.dmsAuth.disabled) {
return { ssoId: 'local-dev', name: '本地开发用户', mobile: '', roles: ['admin'], routes: [] }
}
const tgc = getTgc(req)
if (!tgc) {
const error = new Error('登录状态已失效')
error.statusCode = 401
throw error
}
const fetchSsoUser = async (tgc) => {
let response
try {
response = await axios.get(config.dmsAuth.ssoUserInfoUrl, {
......@@ -52,6 +43,39 @@ export const authenticateRequest = async (req) => {
error.statusCode = 401
throw error
}
return user
}
const getSsoUser = async (tgc) => {
const cached = userCache.get(tgc)
if (cached && cached.expiresAt > Date.now()) return cached.value
const pending = userRequests.get(tgc)
if (pending) return pending
const request = fetchSsoUser(tgc)
.then((user) => {
userCache.set(tgc, {
value: user,
expiresAt: Date.now() + config.dmsAuth.userInfoCacheTtlSeconds * 1000,
})
return user
})
.finally(() => userRequests.delete(tgc))
userRequests.set(tgc, request)
return request
}
export const authenticateRequest = async (req) => {
const tgc = getTgc(req)
if (!tgc) {
const error = new Error('登录状态已失效')
error.statusCode = 401
throw error
}
const user = await getSsoUser(tgc)
let authorization
try {
......
......@@ -5,6 +5,7 @@ import { createPermissionSignature } from './permission-sign.js'
import { getUserProfiles } from './user-center.client.js'
const cache = new Map()
const requests = new Map()
const unwrapItems = (payload) => payload?.data?.items ?? []
......@@ -40,19 +41,28 @@ export const getUserAuthorization = async (ssoId, tgc) => {
const cached = cache.get(ssoId)
if (cached && cached.expiresAt > Date.now()) return cached.value
const [roles, routes] = await Promise.all([
const pending = requests.get(ssoId)
if (pending) return pending
const requestPromise = Promise.all([
request('/api/v1/user/roles', tgc),
request('/api/v1/user/routes', tgc),
])
const value = {
roles: Array.isArray(roles) ? roles : [],
routes: Array.isArray(routes) ? routes : [],
}
cache.set(ssoId, {
value,
expiresAt: Date.now() + config.dmsAuth.permissionCacheTtlSeconds * 1000,
})
return value
.then(([roles, routes]) => {
const value = {
roles: Array.isArray(roles) ? roles : [],
routes: Array.isArray(routes) ? routes : [],
}
cache.set(ssoId, {
value,
expiresAt: Date.now() + config.dmsAuth.permissionCacheTtlSeconds * 1000,
})
return value
})
.finally(() => requests.delete(ssoId))
requests.set(ssoId, requestPromise)
return requestPromise
}
export const getApplicationUsers = async (tgc, params = {}) => {
......
......@@ -8,6 +8,11 @@ export const list = async (req, res) => {
success(res, result)
}
export const sync = async (_req, res) => {
const result = await casesService.syncCasesToPublic()
success(res, result)
}
export const detail = async (req, res) => {
const record = await casesService.getCaseById(req.params.id)
if (!record) return error(res, '案例不存在', 404)
......
import { and, count, desc, eq, like, or } from 'drizzle-orm'
import config from '../../../config.js'
import { httpError } from '../../../lib/http-error.js'
import { uploadJson } from '../../../lib/oss.js'
import { db } from '../../../db/index.js'
import { cases } from './schema.js'
import { products } from '../products/schema.js'
const parseFiles = (row) => {
if (!row) return row
......@@ -12,7 +15,25 @@ const parseFiles = (row) => {
}
}
const rowsToList = (rows) => rows.map(parseFiles)
const rowsToList = (rows) => rows.map((row) => parseFiles({
...row.caseRecord,
productName: row.productName || '',
}))
const toPublicCase = (record) => ({
id: record.id,
name: record.name,
description: record.description || '',
product_id: record.productId ?? null,
product_name: record.productName || '',
files: record.files || [],
operator: {
id: record.operatorUserId || '',
name: record.operatorName || '',
},
created_at: record.createdAt,
updated_at: record.updatedAt,
})
const resolveOperator = (currentUser = {}) => ({
operatorUserId: currentUser.userId || null,
......@@ -39,7 +60,14 @@ export const listCases = async (filters = {}, options = {}) => {
const where = buildFilters(filters)
const [list, totalRows] = await Promise.all([
db.select().from(cases).where(where).orderBy(desc(cases.updatedAt)).limit(limit).offset(offset),
db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.where(where)
.orderBy(desc(cases.updatedAt))
.limit(limit)
.offset(offset),
db.select({ total: count() }).from(cases).where(where),
])
const total = totalRows[0]?.total || 0
......@@ -55,10 +83,37 @@ export const listCases = async (filters = {}, options = {}) => {
}
}
export const syncCasesToPublic = async () => {
const rows = await db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.orderBy(desc(cases.id))
const payload = rows.map((row) => toPublicCase(parseFiles({
...row.caseRecord,
productName: row.productName || '',
})))
const upload = await uploadJson(config.oss.caseJsonObjectKey, payload)
return {
count: payload.length,
...upload,
}
}
export const getCaseById = async (id) => {
const rows = await db.select().from(cases).where(eq(cases.id, Number(id))).limit(1)
const rows = await db
.select({ caseRecord: cases, productName: products.name })
.from(cases)
.leftJoin(products, eq(cases.productId, products.id))
.where(eq(cases.id, Number(id)))
.limit(1)
return parseFiles(rows[0]) || null
if (!rows[0]) return null
return parseFiles({
...rows[0].caseRecord,
productName: rows[0].productName || '',
})
}
export const createCase = async (payload = {}, currentUser = {}) => {
......
......@@ -59,6 +59,7 @@ router.use('/dms/products', requireRouteAccess('/dms/products'), productsManageR
// ── cases ──
const casesRouter = Router()
casesRouter.get('/', validate({ query: casesListQuery }), casesController.list)
casesRouter.post('/sync', casesController.sync)
casesRouter.post('/', validate({ body: caseCreateBody }), casesController.create)
casesRouter.get('/:id', validate({ params: idParam }), casesController.detail)
casesRouter.put('/:id', validate({ params: idParam, body: caseUpdateBody }), casesController.update)
......
......@@ -155,7 +155,7 @@ const getProjectByCodeWithDb = async (db, projectCode) => {
return {
...rows[0].project,
productName: rows[0].productName || rows[0].project.productName,
productName: rows[0].productName || '',
}
}
......@@ -341,7 +341,7 @@ export const listProjects = async (filters = {}, options = {}, currentUser = {})
&& [PROJECT_STAGES.SOLUTION, PROJECT_STAGES.INITIATION].includes(row.project.stage)
&& assignmentsByProjectId.get(row.project.id)?.some((item) => item.role === 'project_manager' && item.ssoId === currentUser.userId)
),
productName: row.productName || row.project.productName,
productName: row.productName || '',
}))
const total = totalRows[0]?.total || 0
......
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论