feat:新增author和question接口

This commit is contained in:
itchenliang 2024-04-18 17:51:25 +08:00
parent f0dbbef0c0
commit 0744613e97
17 changed files with 1354 additions and 1 deletions

119
package-lock.json generated Normal file
View 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"
}
}
}
}

View File

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

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

View File

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

View File

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

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

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

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

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

View File

@ -0,0 +1 @@
export class UpdateAuthorDto {}

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

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

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

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

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

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

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