mirror of
https://github.com/ischenliang/quickly-picture-bed.git
synced 2024-11-25 16:22:44 +08:00
feat:新增author和question接口
This commit is contained in:
parent
f0dbbef0c0
commit
0744613e97
119
package-lock.json
generated
Normal file
119
package-lock.json
generated
Normal file
@ -0,0 +1,119 @@
|
||||
{
|
||||
"name": "quickly-picture-bed",
|
||||
"version": "2.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "quickly-picture-bed",
|
||||
"version": "2.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nestjs/mapped-types": "*"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.15.0",
|
||||
"npm": ">=9.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
"integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/common": {
|
||||
"version": "10.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz",
|
||||
"integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iterare": "1.2.1",
|
||||
"tslib": "2.6.2",
|
||||
"uid": "2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0",
|
||||
"rxjs": "^7.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
||||
"integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iterare": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz",
|
||||
"integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/uid": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz",
|
||||
"integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@lukeed/csprng": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@nestjs/mapped-types": "*"
|
||||
},
|
||||
"name": "quickly-picture-bed",
|
||||
"version": "2.0.0",
|
||||
"description": "轻快图片管理系统2.0版本",
|
||||
|
231
server/package-lock.json
generated
231
server/package-lock.json
generated
@ -18,10 +18,13 @@
|
||||
"@nestjs/mapped-types": "^2.0.4",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@nestjs/sequelize": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.1.8",
|
||||
"axios": "^1.4.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"compression": "^1.7.4",
|
||||
"cron": "^3.1.7",
|
||||
"csurf": "^1.11.0",
|
||||
"form-data": "^4.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@ -1942,6 +1945,31 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.0.2.tgz",
|
||||
"integrity": "sha512-po9oauE7fO0CjhDKvVC2tzEgjOUwhxYoIsXIVkgfu+xaDMmzzpmXY2s1LT4oP90Z+PaTtPoAHmhslnYmo4mSZg==",
|
||||
"dependencies": {
|
||||
"cron": "3.1.7",
|
||||
"uuid": "9.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schedule/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/schematics": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/schematics/-/schematics-10.1.0.tgz",
|
||||
@ -2575,6 +2603,11 @@
|
||||
"integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA=="
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz",
|
||||
@ -3499,6 +3532,11 @@
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
@ -3691,6 +3729,42 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"parse5": "^7.0.0",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.5.3.tgz",
|
||||
@ -4094,6 +4168,15 @@
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cron": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/cron/-/cron-3.1.7.tgz",
|
||||
"integrity": "sha512-tlBg7ARsAMQLzgwqVxy8AZl/qlTc5nibqYwtNGoCrd+cV+ugI+tvZC1oT/8dFH8W455YrywGykx/KMmAqOr7Jw==",
|
||||
"dependencies": {
|
||||
"@types/luxon": "~3.4.0",
|
||||
"luxon": "~3.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz",
|
||||
@ -4144,6 +4227,32 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/csurf": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/csurf/-/csurf-1.11.0.tgz",
|
||||
@ -4390,6 +4499,57 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz",
|
||||
"integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.3.1.tgz",
|
||||
@ -4480,6 +4640,17 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@ -5593,6 +5764,24 @@
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
@ -6986,6 +7175,14 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
|
||||
"integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.5",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.5.tgz",
|
||||
@ -7373,6 +7570,17 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -7550,6 +7758,29 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
|
||||
"integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
|
||||
"dependencies": {
|
||||
"entities": "^4.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz",
|
||||
"integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.2",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
@ -19,10 +19,13 @@
|
||||
"@nestjs/mapped-types": "^2.0.4",
|
||||
"@nestjs/passport": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.0.2",
|
||||
"@nestjs/sequelize": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.1.8",
|
||||
"axios": "^1.4.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"compression": "^1.7.4",
|
||||
"cron": "^3.1.7",
|
||||
"csurf": "^1.11.0",
|
||||
"form-data": "^4.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
|
@ -20,6 +20,8 @@ import { LogModule } from './log/log.module';
|
||||
import { StatsModule } from './stats/stats.module';
|
||||
import { SmsCode } from './common/entities/smsCode.entity';
|
||||
import { User } from './user/entities/user.entity';
|
||||
import { QuestionModule } from './question/question.module';
|
||||
import { AuthorModule } from './author/author.module';
|
||||
|
||||
console.log(process.env.NODE_ENV)
|
||||
@Module({
|
||||
@ -66,7 +68,9 @@ console.log(process.env.NODE_ENV)
|
||||
ImageModule,
|
||||
LogModule,
|
||||
StatsModule,
|
||||
SequelizeModule.forFeature([SmsCode, User])
|
||||
SequelizeModule.forFeature([SmsCode, User]),
|
||||
QuestionModule,
|
||||
AuthorModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
81
server/src/author/author.controller.ts
Normal file
81
server/src/author/author.controller.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { AuthorService } from './author.service';
|
||||
import { CreateAuthorDto } from './dto/create-author.dto';
|
||||
import { UpdateAuthorDto } from './dto/update-author.dto';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from 'src/auth/local-auth.guard';
|
||||
import { RoleGuard } from 'src/common/role.guard';
|
||||
import { User } from 'src/common/user.decorator';
|
||||
import { User as UserType } from 'src/user/entities/user.entity'
|
||||
|
||||
@Controller({ path: 'author', version: '1' })
|
||||
@ApiTags('知乎作者管理')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RoleGuard)
|
||||
export class AuthorController {
|
||||
constructor(private readonly authorService: AuthorService) {}
|
||||
|
||||
@Post('create')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '新增作者', description: '新增作者' })
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
create(@Body() createAuthorDto: CreateAuthorDto, @User() user: UserType) {
|
||||
return this.authorService.create(createAuthorDto, user.id);
|
||||
}
|
||||
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '作者列表', description: '查询作者列表' })
|
||||
@ApiResponse({ status: 200, description: '查询成功' })
|
||||
findAll(@Body() param: any, @User() user: UserType) {
|
||||
return this.authorService.findAll(param, user.id);
|
||||
}
|
||||
|
||||
@Post('detail')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '作者详情', description: '查询作者详情' })
|
||||
@ApiResponse({ status: 200, description: '查询成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '作者id'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findOne(@Body('id') id: number, @User() user: UserType) {
|
||||
return this.authorService.findOne(id, user.id);
|
||||
}
|
||||
|
||||
@Post('update')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '更新作者', description: '更新作者' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
update(@Body() param: CreateAuthorDto, @User() user: UserType) {
|
||||
return this.authorService.update(param, user.id);
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '删除作者', description: '删除作者' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '作者id'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
remove(@Body('id') id: number, @User() user: UserType) {
|
||||
return this.authorService.remove(id, user.id);
|
||||
}
|
||||
}
|
14
server/src/author/author.module.ts
Normal file
14
server/src/author/author.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthorService } from './author.service';
|
||||
import { AuthorController } from './author.controller';
|
||||
import { SequelizeModule } from '@nestjs/sequelize';
|
||||
import { Author } from './entities/author.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([Author])
|
||||
],
|
||||
controllers: [AuthorController],
|
||||
providers: [AuthorService],
|
||||
})
|
||||
export class AuthorModule {}
|
249
server/src/author/author.service.ts
Normal file
249
server/src/author/author.service.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AuthorFilter, CreateAuthorDto } from './dto/create-author.dto';
|
||||
import { UpdateAuthorDto } from './dto/update-author.dto';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { Author } from './entities/author.entity';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio'
|
||||
import { CronJob } from 'cron';
|
||||
import * as nodemailer from 'nodemailer'
|
||||
import { Op } from 'sequelize';
|
||||
const cron = '10 * * * * *'
|
||||
const email = 'itchenliang@163.com'
|
||||
|
||||
@Injectable()
|
||||
export class AuthorService {
|
||||
private readonly logger = new Logger(AuthorService.name)
|
||||
constructor (
|
||||
@InjectModel(Author) private authorModel: typeof Author,
|
||||
private scheduleRegistry: SchedulerRegistry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建
|
||||
* @param createAuthorDto
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async create(createAuthorDto: CreateAuthorDto, uid: number) {
|
||||
const data = await this.authorModel.create({
|
||||
...createAuthorDto,
|
||||
notify_status: false,
|
||||
status: true,
|
||||
uid
|
||||
})
|
||||
this.startNotify(cron, data, uid)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
* @param param
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async findAll(param: AuthorFilter, uid: number) {
|
||||
const { page, size, search, status } = param
|
||||
const data: any = {}
|
||||
const tmp: any = {
|
||||
order: [
|
||||
['createdAt', 'desc']
|
||||
],
|
||||
where: {
|
||||
uid: uid,
|
||||
author_id: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
},
|
||||
author_name: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
},
|
||||
author_avatar: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
},
|
||||
last_question_id: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(param).includes('status')) {
|
||||
tmp.where.status = status
|
||||
}
|
||||
if (page) {
|
||||
tmp.limit = size || 10
|
||||
tmp.offset = page ? (page - 1) * size : 0
|
||||
}
|
||||
const { count, rows } = await this.authorModel.findAndCountAll(tmp)
|
||||
data.total = count
|
||||
data.items = rows
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 详情
|
||||
* @param id
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
findOne(id: number, uid: number) {
|
||||
return this.authorModel.findOne({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param id
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async update(param: CreateAuthorDto, uid: number) {
|
||||
const data = await this.authorModel.update({
|
||||
...param
|
||||
}, {
|
||||
where: {
|
||||
id: param.id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
const res = await this.findOne(param.id, uid)
|
||||
this.stopNotify(res.author_id)
|
||||
this.deleteNotify(res.author_id)
|
||||
this.startNotify(cron, res, uid)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除问题
|
||||
* @param id
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async remove(id: number, uid: number) {
|
||||
const data = await this.authorModel.findOne({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
if (data) {
|
||||
this.stopNotify(data.author_id)
|
||||
this.deleteNotify(data.author_id)
|
||||
}
|
||||
return this.authorModel.destroy({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param text 验证码
|
||||
* @param to 收件人邮箱
|
||||
* @param subject 标题
|
||||
* @returns
|
||||
*/
|
||||
sendMail (text: string, to: string, subject: string = 'LightFastPicture') {
|
||||
var user = '1825956830@qq.com' // 自己的邮箱
|
||||
var pass = 'stjflvegjjumbbfa' // 邮箱授权码
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: "smtp.qq.com",
|
||||
port: 587,
|
||||
secure: false,
|
||||
//配置发送者的邮箱服务器和登录信息
|
||||
// service:'qq', // 163、qq等
|
||||
auth: {
|
||||
user: user, // 用户账号
|
||||
pass: pass, //授权码,通过QQ获取
|
||||
},
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pass && user) {
|
||||
transporter.sendMail({
|
||||
from: `<${user}>`,
|
||||
to: `<${to}>`,
|
||||
subject: subject,
|
||||
html: `${text}`,
|
||||
}).then(() => {
|
||||
resolve(true)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
} else {
|
||||
reject(new Error('未配置邮件服务'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始通知:创建定时任务
|
||||
* @param time
|
||||
* @param question_id
|
||||
*/
|
||||
startNotify (time: string, author: CreateAuthorDto, uid: number) {
|
||||
const { author_id, last_question_id, is_org, author_name, id } = author
|
||||
const job = new CronJob(time, async () => {
|
||||
// 在这里编写查询是否变红包任务的逻辑
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `https://www.zhihu.com/${is_org ? 'org' : 'people'}/${author_id}/asks`
|
||||
})
|
||||
const $ = cheerio.load(res.data)
|
||||
const initialDataEl = $('script#js-initialData')
|
||||
const initialDataJson = initialDataEl.text()
|
||||
const initialData = JSON.parse(initialDataJson)
|
||||
const questions = initialData.initialState.entities.questions
|
||||
const ids = Object.keys(questions).filter(id => questions[id].questionType === 'commercial')
|
||||
const commercials = ids.map(id => questions[id]).sort((a, b) => b.created - a.created)
|
||||
if (commercials.length) {
|
||||
// 有新的问题
|
||||
const { id: question_id, title } = commercials[0]
|
||||
if (question_id !== last_question_id) {
|
||||
await this.sendMail(`【${author_name}】新添加了一个问题:${title},<a href="https://www.zhihu.com/question/${question_id}" target="_blank">赶快前往去回答吧</a>`, email)
|
||||
await this.authorModel.update({
|
||||
last_question_id: question_id
|
||||
}, {
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 没有新增问题:继续定时任务
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
|
||||
this.scheduleRegistry.addCronJob(author_id, job)
|
||||
job.start()
|
||||
this.logger.warn(`job ${author_id} added!`)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 暂停通知
|
||||
* @param question_id
|
||||
*/
|
||||
stopNotify (question_id: string) {
|
||||
const job = this.scheduleRegistry.getCronJob(question_id)
|
||||
job && job.stop()
|
||||
this.logger.warn(`job ${question_id} stopped!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知
|
||||
* @param question_id
|
||||
*/
|
||||
deleteNotify (question_id: string) {
|
||||
this.scheduleRegistry.deleteCronJob(question_id)
|
||||
this.logger.warn(`job ${question_id} deleted!`)
|
||||
}
|
||||
}
|
31
server/src/author/dto/create-author.dto.ts
Normal file
31
server/src/author/dto/create-author.dto.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { PageSearch } from "src/common/dto/pageSearch.entity";
|
||||
|
||||
export class CreateAuthorDto {
|
||||
@ApiProperty({ description: 'id' })
|
||||
id?: number
|
||||
|
||||
@ApiProperty({ description: '作者id' })
|
||||
author_id: string
|
||||
|
||||
@ApiProperty({ description: '作者名称' })
|
||||
author_name: string
|
||||
|
||||
@ApiProperty({ description: '作者头像' })
|
||||
author_avatar: string
|
||||
|
||||
@ApiProperty({ description: '是否为机构账号' })
|
||||
is_org: boolean
|
||||
|
||||
@ApiProperty({ description: '最新问题id' })
|
||||
last_question_id: string
|
||||
|
||||
@ApiProperty({ description: '通知状态' })
|
||||
status?: boolean
|
||||
}
|
||||
|
||||
|
||||
export class AuthorFilter extends PageSearch {
|
||||
@ApiProperty({ description: '状态' })
|
||||
status: boolean
|
||||
}
|
1
server/src/author/dto/update-author.dto.ts
Normal file
1
server/src/author/dto/update-author.dto.ts
Normal file
@ -0,0 +1 @@
|
||||
export class UpdateAuthorDto {}
|
66
server/src/author/entities/author.entity.ts
Normal file
66
server/src/author/entities/author.entity.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { BelongsTo, Column, ForeignKey, Table, Model, HasMany, DataType } from "sequelize-typescript";
|
||||
import { User } from "src/user/entities/user.entity";
|
||||
|
||||
@Table({ tableName: 'author' })
|
||||
export class Author extends Model<Author> {
|
||||
@Column({
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
})
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '作者id'
|
||||
})
|
||||
author_id: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '作者名称'
|
||||
})
|
||||
author_name: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '作者头像'
|
||||
})
|
||||
author_avatar: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '是否为机构账号',
|
||||
defaultValue: false
|
||||
})
|
||||
is_org: boolean
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '最近一个问题id'
|
||||
})
|
||||
last_question_id: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '通知状态',
|
||||
defaultValue: false
|
||||
})
|
||||
notify_status: boolean
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '状态',
|
||||
defaultValue: true
|
||||
})
|
||||
status: boolean
|
||||
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column({
|
||||
comment: '创建人'
|
||||
})
|
||||
uid: number
|
||||
|
||||
@BelongsTo(() => User, 'uid')
|
||||
user: User
|
||||
}
|
13
server/src/question/dto/create-question.dto.ts
Normal file
13
server/src/question/dto/create-question.dto.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { PageSearch } from "src/common/dto/pageSearch.entity";
|
||||
|
||||
export class CreateQuestionDto {
|
||||
@ApiProperty({ description: '问题id' })
|
||||
quesion_id: string
|
||||
}
|
||||
|
||||
|
||||
export class QuestionFilter extends PageSearch {
|
||||
@ApiProperty({ description: '状态' })
|
||||
status: boolean
|
||||
}
|
16
server/src/question/dto/update-question.dto.ts
Normal file
16
server/src/question/dto/update-question.dto.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateQuestionDto {
|
||||
|
||||
@ApiProperty({ description: '红包金额' })
|
||||
question_red_money?: number
|
||||
|
||||
@ApiProperty({ description: '红包数量' })
|
||||
question_red_count?: number
|
||||
|
||||
@ApiProperty({ description: '通知状态' })
|
||||
notify_status?: boolean
|
||||
|
||||
@ApiProperty({ description: '状态' })
|
||||
status?: boolean
|
||||
}
|
96
server/src/question/entities/question.entity.ts
Normal file
96
server/src/question/entities/question.entity.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { BelongsTo, Column, ForeignKey, Table, Model, HasMany, DataType } from "sequelize-typescript";
|
||||
import { User } from "src/user/entities/user.entity";
|
||||
|
||||
@Table({ tableName: 'question' })
|
||||
export class Question extends Model<Question> {
|
||||
@Column({
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
})
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题原id'
|
||||
})
|
||||
quesion_id: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题标题'
|
||||
})
|
||||
question_title: string
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '问题描述'
|
||||
})
|
||||
question_desc: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题作者名称'
|
||||
})
|
||||
question_author_name: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题作者id'
|
||||
})
|
||||
question_author_id: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题作者头像'
|
||||
})
|
||||
question_author_avatar: string
|
||||
|
||||
@Column({
|
||||
allowNull: false,
|
||||
comment: '问题创建时间'
|
||||
})
|
||||
question_created: string
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '问题更新时间'
|
||||
})
|
||||
question_updated: string
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '问题红包金额',
|
||||
defaultValue: 0
|
||||
})
|
||||
question_red_money: number
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '问题红包个数',
|
||||
defaultValue: 0
|
||||
})
|
||||
question_red_count: number
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '通知状态',
|
||||
defaultValue: false
|
||||
})
|
||||
notify_status: boolean
|
||||
|
||||
@Column({
|
||||
allowNull: true,
|
||||
comment: '状态',
|
||||
defaultValue: true
|
||||
})
|
||||
status: boolean
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column({
|
||||
comment: '创建人'
|
||||
})
|
||||
uid: number
|
||||
|
||||
@BelongsTo(() => User, 'uid')
|
||||
user: User
|
||||
}
|
119
server/src/question/question.controller.ts
Normal file
119
server/src/question/question.controller.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, HttpCode } from '@nestjs/common';
|
||||
import { QuestionService } from './question.service';
|
||||
import { CreateQuestionDto, QuestionFilter } from './dto/create-question.dto';
|
||||
import { UpdateQuestionDto } from './dto/update-question.dto';
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from 'src/auth/local-auth.guard';
|
||||
import { RoleGuard } from 'src/common/role.guard';
|
||||
import { User } from 'src/common/user.decorator';
|
||||
import { User as UserType } from 'src/user/entities/user.entity'
|
||||
|
||||
@Controller({ path: 'question', version: '1' })
|
||||
@ApiTags('知乎问题管理')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard, RoleGuard)
|
||||
export class QuestionController {
|
||||
constructor(private readonly questionService: QuestionService) {}
|
||||
|
||||
@Post('create')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '创建问题', description: '创建问题' })
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
create(@Body() createQuestionDto: CreateQuestionDto, @User() user: UserType) {
|
||||
return this.questionService.create(createQuestionDto, user.id);
|
||||
}
|
||||
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '问题列表', description: '查询问题列表' })
|
||||
@ApiResponse({ status: 200, description: '查询成功' })
|
||||
findAll(@Body() param: QuestionFilter, @User() user: UserType) {
|
||||
return this.questionService.findAll(param, user.id);
|
||||
}
|
||||
|
||||
@Post('detail')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '问题详情', description: '查询问题详情' })
|
||||
@ApiResponse({ status: 200, description: '查询成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '问题id'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
findOne(@Body('id') id: number, @User() user: UserType) {
|
||||
return this.questionService.findOne(id, user.id);
|
||||
}
|
||||
|
||||
@Post('delete')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '删除问题', description: '删除问题' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'number',
|
||||
default: 1,
|
||||
description: '问题id'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
remove(@Body('id') id: number, @User() user: UserType) {
|
||||
return this.questionService.remove(id, user.id);
|
||||
}
|
||||
|
||||
@Post('startSchedule')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '启用定时任务', description: '启用定时任务' })
|
||||
@ApiResponse({ status: 200, description: '启用成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'number',
|
||||
},
|
||||
default: [],
|
||||
description: '问题ids'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
startSchedule(@Body('ids') ids: number[], @User() user: UserType) {
|
||||
return this.questionService.startSchedule(ids, user.id)
|
||||
}
|
||||
|
||||
@Post('stopSchedule')
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ summary: '关闭定时任务', description: '关闭定时任务' })
|
||||
@ApiResponse({ status: 200, description: '关闭成功' })
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ids: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'number',
|
||||
},
|
||||
default: [],
|
||||
description: '问题ids'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
stopSchedule(@Body('ids') ids: number[], @User() user: UserType) {
|
||||
return this.questionService.stopSchedule(ids, user.id)
|
||||
}
|
||||
}
|
17
server/src/question/question.module.ts
Normal file
17
server/src/question/question.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { QuestionService } from './question.service';
|
||||
import { QuestionController } from './question.controller';
|
||||
import { SequelizeModule } from '@nestjs/sequelize';
|
||||
import { Question } from './entities/question.entity';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([Question]),
|
||||
ScheduleModule.forRoot()
|
||||
],
|
||||
controllers: [QuestionController],
|
||||
providers: [QuestionService],
|
||||
exports: [QuestionService]
|
||||
})
|
||||
export class QuestionModule {}
|
290
server/src/question/question.service.ts
Normal file
290
server/src/question/question.service.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { CreateQuestionDto, QuestionFilter } from './dto/create-question.dto';
|
||||
import { UpdateQuestionDto } from './dto/update-question.dto';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { Question } from './entities/question.entity';
|
||||
import axios from 'axios';
|
||||
import * as cheerio from 'cheerio'
|
||||
import { Op } from 'sequelize';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { CronJob } from 'cron';
|
||||
import * as nodemailer from 'nodemailer'
|
||||
const cron = '10 * * * * *'
|
||||
|
||||
@Injectable()
|
||||
export class QuestionService {
|
||||
private readonly logger = new Logger(QuestionService.name)
|
||||
constructor (
|
||||
@InjectModel(Question) private questionModel: typeof Question,
|
||||
private scheduleRegistry: SchedulerRegistry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建问题
|
||||
* @param createQuestionDto
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async create(createQuestionDto: CreateQuestionDto, uid: number) {
|
||||
try {
|
||||
// 自动获取问题内容
|
||||
const res = await axios({
|
||||
url: `https://www.zhihu.com/question/${createQuestionDto.quesion_id}`,
|
||||
method: 'get'
|
||||
})
|
||||
const $ = cheerio.load(res.data)
|
||||
const initialDataEl = $('script#js-initialData')
|
||||
const initialDataJson = initialDataEl.text()
|
||||
const initialData = JSON.parse(initialDataJson)
|
||||
const { title, excerpt, author, created, updatedTime } = initialData.initialState.entities.questions[createQuestionDto.quesion_id]
|
||||
const question = await this.questionModel.create({
|
||||
quesion_id: createQuestionDto.quesion_id,
|
||||
question_title: title,
|
||||
question_desc: excerpt || '',
|
||||
question_author_id: author.urlToken || author.id,
|
||||
question_author_name: author.name || '',
|
||||
question_author_avatar: author.avatarUrl || author.avatarUrlTemplate,
|
||||
question_created: created,
|
||||
question_updated: updatedTime,
|
||||
uid: uid
|
||||
})
|
||||
// 创建完任务后立马启用通知
|
||||
this.startNotify(cron, createQuestionDto.quesion_id, uid)
|
||||
return question
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
data: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询列表
|
||||
* @param param
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async findAll(param: QuestionFilter, uid: number) {
|
||||
const { page, size, search, status } = param
|
||||
const data: any = {}
|
||||
const tmp: any = {
|
||||
order: [
|
||||
['question_updated', 'desc']
|
||||
],
|
||||
where: {
|
||||
uid: uid,
|
||||
question_title: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
},
|
||||
question_desc: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
},
|
||||
question_author_name: {
|
||||
[Op.like]: search ? `%${search}%` : '%%'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(param).includes('status')) {
|
||||
tmp.where.status = status
|
||||
}
|
||||
if (page) {
|
||||
tmp.limit = size || 10
|
||||
tmp.offset = page ? (page - 1) * size : 0
|
||||
}
|
||||
const { count, rows } = await this.questionModel.findAndCountAll(tmp)
|
||||
data.total = count
|
||||
data.items = rows
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 问题详情
|
||||
* @param id
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
findOne(id: number, uid: number) {
|
||||
return this.questionModel.findOne({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除问题
|
||||
* @param id
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async remove(id: number, uid: number) {
|
||||
const data = await this.questionModel.findOne({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
if (data) {
|
||||
this.stopNotify(data.quesion_id)
|
||||
this.deleteNotify(data.quesion_id)
|
||||
}
|
||||
return this.questionModel.destroy({
|
||||
where: {
|
||||
id,
|
||||
uid
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param updateDto
|
||||
* @param quesion_id
|
||||
* @param uid
|
||||
*/
|
||||
update (updateDto: UpdateQuestionDto, quesion_id: string, uid: number) {
|
||||
return this.questionModel.update({
|
||||
...updateDto
|
||||
}, {
|
||||
where: {
|
||||
quesion_id: quesion_id,
|
||||
uid
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param text 验证码
|
||||
* @param to 收件人邮箱
|
||||
* @param subject 标题
|
||||
* @returns
|
||||
*/
|
||||
sendMail (text: string, to: string, subject: string = 'LightFastPicture') {
|
||||
var user = '1825956830@qq.com' // 自己的邮箱
|
||||
var pass = 'stjflvegjjumbbfa' // 邮箱授权码
|
||||
let transporter = nodemailer.createTransport({
|
||||
host: "smtp.qq.com",
|
||||
port: 587,
|
||||
secure: false,
|
||||
//配置发送者的邮箱服务器和登录信息
|
||||
// service:'qq', // 163、qq等
|
||||
auth: {
|
||||
user: user, // 用户账号
|
||||
pass: pass, //授权码,通过QQ获取
|
||||
},
|
||||
})
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pass && user) {
|
||||
transporter.sendMail({
|
||||
from: `<${user}>`,
|
||||
to: `<${to}>`,
|
||||
subject: subject,
|
||||
html: `${text}`,
|
||||
}).then(() => {
|
||||
resolve(true)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
} else {
|
||||
reject(new Error('未配置邮件服务'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始通知:创建定时任务
|
||||
* @param time
|
||||
* @param question_id
|
||||
*/
|
||||
startNotify (time: string, question_id: string, uid: number) {
|
||||
const job = new CronJob(time, async () => {
|
||||
// 在这里编写查询是否变红包任务的逻辑
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `https://www.zhihu.com/api/v4/brand/questions/${question_id}/activity/red-packet`
|
||||
})
|
||||
const { content, title, count_down_value } = res.data
|
||||
if (count_down_value) {
|
||||
// 第一步:邮箱通知
|
||||
await this.sendMail(content, 'itchenliang@163.com')
|
||||
// 第二步:更新状态
|
||||
let money = 0
|
||||
const match = title.match(/\d+/)
|
||||
if (match) {
|
||||
money = parseInt(match[0])
|
||||
await this.update({
|
||||
question_red_money: money,
|
||||
question_red_count: count_down_value,
|
||||
notify_status: true
|
||||
}, question_id, uid)
|
||||
}
|
||||
// 第三步:关闭并删除定时任务
|
||||
this.stopNotify(question_id)
|
||||
this.deleteNotify(question_id)
|
||||
}
|
||||
} catch (error) {
|
||||
// 不是红包问题:继续定时任务
|
||||
console.log(error)
|
||||
}
|
||||
})
|
||||
this.scheduleRegistry.addCronJob(question_id, job)
|
||||
job.start()
|
||||
this.logger.warn(`job ${question_id} added!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停通知
|
||||
* @param question_id
|
||||
*/
|
||||
stopNotify (question_id: string) {
|
||||
const job = this.scheduleRegistry.getCronJob(question_id)
|
||||
job && job.stop()
|
||||
this.logger.warn(`job ${question_id} stopped!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知
|
||||
* @param question_id
|
||||
*/
|
||||
deleteNotify (question_id: string) {
|
||||
this.scheduleRegistry.deleteCronJob(question_id)
|
||||
this.logger.warn(`job ${question_id} deleted!`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启定时任务
|
||||
* @param ids
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async startSchedule (ids: number[], uid) {
|
||||
const promises = ids.map(id => this.findOne(id, uid))
|
||||
const quesions = await Promise.all(promises)
|
||||
const questionPromises = quesions.filter(quesion => quesion && quesion.id && !quesion.notify_status).map(quesion => {
|
||||
this.startNotify(cron, quesion.quesion_id, uid)
|
||||
return this.update({ status: true }, quesion.quesion_id, uid)
|
||||
})
|
||||
return Promise.all(questionPromises)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭定时任务
|
||||
* @param ids
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async stopSchedule (ids: number[], uid) {
|
||||
const promises = ids.map(id => this.findOne(id, uid))
|
||||
const quesions = await Promise.all(promises)
|
||||
const questionPromises = quesions.filter(quesion => quesion && quesion.id && !quesion.notify_status).map(quesion => {
|
||||
this.stopNotify(quesion.quesion_id)
|
||||
this.deleteNotify(quesion.quesion_id)
|
||||
return this.update({ status: false }, quesion.quesion_id, uid)
|
||||
})
|
||||
return Promise.all(questionPromises)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user