├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── env_template ├── nest-cli.json ├── package.json ├── pm2.conf.json ├── pnpm-lock.yaml ├── src ├── app.module.ts ├── cache │ └── cache.service.ts ├── common │ └── entity │ │ └── baseEntity.ts ├── config │ ├── database.ts │ ├── email.ts │ ├── jwt.ts │ ├── redis.ts │ ├── statusMonitor.ts │ └── upload.ts ├── constant │ ├── avatar.ts │ └── collec.ts ├── decorator │ └── ipAddress.guard.ts ├── filters │ ├── http-exception.filter.spec.ts │ └── http-exception.filter.ts ├── guard │ ├── auth.guard.spec.ts │ └── auth.guard.ts ├── interceptor │ ├── transform.interceptor.spec.ts │ └── transform.interceptor.ts ├── lib │ └── file.ts ├── main.ts ├── modules │ ├── article │ │ ├── article.controller.ts │ │ ├── article.entity.ts │ │ ├── article.module.ts │ │ ├── article.service.ts │ │ └── dto │ │ │ ├── read.article.dto.ts │ │ │ └── set.article.dto.ts │ ├── chat │ │ ├── chat.controller.ts │ │ ├── chat.getaway.ts │ │ ├── chat.module.ts │ │ ├── chat.service.ts │ │ ├── dto │ │ │ ├── room.dto.ts │ │ │ └── search.dto.ts │ │ ├── message.entity.ts │ │ └── room.entity.ts │ ├── collect │ │ ├── collect.controller.ts │ │ ├── collect.entity.ts │ │ ├── collect.module.ts │ │ ├── collect.service.ts │ │ └── dto │ │ │ └── set.collect.dto.ts │ ├── comment │ │ ├── comment.controller.ts │ │ ├── comment.entity.ts │ │ ├── comment.module.ts │ │ ├── comment.service.ts │ │ └── dto │ │ │ └── comment.set.dto.ts │ ├── common │ │ ├── common.service.ts │ │ └── type.ts │ ├── email │ │ ├── email.controller.ts │ │ ├── email.module.ts │ │ └── email.service.ts │ ├── friend-links │ │ ├── dto │ │ │ └── links.set.dto.ts │ │ ├── friend-links.controller.ts │ │ ├── friend-links.entity.ts │ │ ├── friend-links.module.ts │ │ └── friend-links.service.ts │ ├── music │ │ ├── dto │ │ │ ├── music.dto.ts │ │ │ └── search.dto.ts │ │ ├── music.controller.ts │ │ ├── music.entity.ts │ │ ├── music.module.ts │ │ └── music.service.ts │ ├── project │ │ ├── dto │ │ │ └── project.set.dto.ts │ │ ├── project.controller.ts │ │ ├── project.module.ts │ │ ├── project.service.ts │ │ └── projtct.entity.ts │ ├── resource-type │ │ ├── dto │ │ │ └── resourceType.set.dto.ts │ │ ├── resource-type.controller.ts │ │ ├── resource-type.entity.ts │ │ ├── resource-type.module.ts │ │ └── resource-type.service.ts │ ├── resource │ │ ├── dto │ │ │ └── resource.set.dto.ts │ │ ├── resource.controller.ts │ │ ├── resource.entity.ts │ │ ├── resource.module.ts │ │ └── resource.service.ts │ ├── spider │ │ ├── dto │ │ │ └── url.music.dto.ts │ │ ├── spider.controller.ts │ │ ├── spider.module.ts │ │ └── spider.service.ts │ ├── statistics │ │ ├── statistics.controller.ts │ │ ├── statistics.module.ts │ │ └── statistics.service.ts │ ├── tag │ │ ├── dto │ │ │ └── set.dto.ts │ │ ├── tag.controller.ts │ │ ├── tag.entity.ts │ │ ├── tag.module.ts │ │ └── tag.service.ts │ ├── tools-type │ │ ├── tools-type.controller.ts │ │ ├── tools-type.entity.ts │ │ ├── tools-type.module.ts │ │ └── tools-type.service.ts │ ├── tools │ │ ├── dto │ │ │ └── tools.douyin.dto.ts │ │ ├── tools.controller.ts │ │ ├── tools.entity.ts │ │ ├── tools.module.ts │ │ └── tools.service.ts │ ├── type │ │ ├── dto │ │ │ ├── del.type.dto.ts │ │ │ └── set.type.dto.ts │ │ ├── type.controller.ts │ │ ├── type.entity.ts │ │ ├── type.module.ts │ │ └── type.service.ts │ ├── upload │ │ ├── upload.controller.ts │ │ ├── upload.module.ts │ │ └── upload.service.ts │ ├── user │ │ ├── dto │ │ │ ├── login.user.dto.ts │ │ │ └── register.user.dto.ts │ │ ├── jwt.strategy.ts │ │ ├── local.strategy.ts │ │ ├── user.controller.ts │ │ ├── user.entity.ts │ │ ├── user.module.ts │ │ └── user.service.ts │ └── verify │ │ ├── dto │ │ └── login.user.dto.ts │ │ ├── verify.controller.ts │ │ ├── verify.entity.ts │ │ ├── verify.module.ts │ │ └── verify.service.ts ├── swagger │ └── index.ts ├── tasks │ ├── tasks.module.ts │ └── tasks.service.ts ├── templates │ └── email │ │ └── welcome.pug └── utils │ ├── date.ts │ ├── index.ts │ ├── spider.ts │ ├── tools.ts │ └── verifyToken.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── utils └── Result.js └── views └── account ├── verify_error.ejs └── verify_success.ejs /.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | PREFIX=/docs 3 | 4 | # common 5 | DB_PORT=3306 6 | DB_TYPE=mysql 7 | JWT_SECRET=Snine_blog 8 | JWT_EXPIRESIN=30d 9 | 10 | # dev 正式开发中 测试和生产环境的env文件可以写两份分别在不同环境使用磁盘挂载的方法加入, 正常情况不应该加入git仓库 11 | DB_HOST_DEV=42.192.77.35 12 | USERNAEM_DEV=snine-blog-dev 13 | PASSWORD_DEV=147258369 14 | DB_DATABASE_DEV=snine-blog-dev 15 | DB_SYNC=true 16 | 17 | # REDIS 18 | DEV_REDIS_HOST=******** 19 | DEV_REDIS_PORT=******** 20 | DEV_REDIS_PASS=******** 21 | DEV_REDIS_USER=******** 22 | 23 | #COS 腾讯云对象存储 替换为你自己的 24 | DEV_TENTCENT_SECRET_ID=xxxxxxxxxxxxxxxxxxx 25 | DEV_TENTCENT_SECRET_KEY=xxxxxxxxxxxxxxxxxxx 26 | DEV_COS_BUCKET_PUBLIC=xxxxxxxxxxxxxxxxxxx 27 | DEV_COS_REGION=xxxxxxxxxxxxxxxxxxx 28 | 29 | # 暂未用到 30 | DEV_COS_BUCKET_PRIVATE=******** 31 | DEV_COS_BUCKET_PRIVATE_CDN=******** 32 | DEV_COS_BUCKET_STATIC=******** 33 | 34 | # PRO 35 | DB_HOST_PRO=42.192.77.35 36 | USERNAEM_PRO=snine-blog-dev 37 | PASSWORD_PRO=147258369 38 | DB_DATABASE_PRO=snine-blog-dev 39 | DB_SYNC=false 40 | 41 | #REDIS 42 | PRO_REDIS_HOST=******** 43 | PRO_REDIS_PORT=******** 44 | PRO_REDIS_PASS=******** 45 | PRO_REDIS_USER=******** 46 | 47 | #COS 腾讯云对象存储 48 | PRO_TENTCENT_SECRET_ID=xxxxxxxxxxxxxxxx 49 | PRO_TENTCENT_SECRET_KEY=xxxxxxxxxxxxxxxxx 50 | PRO_COS_BUCKET_PUBLIC=xxxxxxxxxxxxxxxxxxx 51 | PRO_COS_REGION=xxxxxxxxxxxxxxxxxxx 52 | 53 | # 暂未用到 54 | PRO_COS_BUCKET_PRIVATE=******** 55 | PRO_COS_BUCKET_PRIVATE_CDN=******** 56 | PRO_COS_BUCKET_STATIC=******** -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /node_modules 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | pnpm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | *.sublime-workspace 27 | 28 | dist* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "tabWidth": 2, 6 | "useTabs": true, 7 | "semi": true, 8 | "quoteProps": "as-needed", 9 | "jsxSingleQuote": false, 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "always", 13 | "eslintIntegration": true, 14 | "endOfLine": "auto", 15 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["html", "vue", "javascript", "jsx"], 3 | "emmet.syntaxProfiles": { 4 | "vue-html": "html", 5 | "vue": "html" 6 | }, 7 | "eslint.alwaysShowStatus": true, 8 | "eslint.quiet": true, 9 | "editor.codeActionsOnSave": { 10 | "source.fixAll.eslint": true, 11 | "source.fixAll": true, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 项目说明 2 | 3 | ### 显示预览 4 | 5 | 前台对外显示地址 [https://jiangly.com](https://jiangly.com) 6 | 7 | 后台管理系统地址 [https://admin.jiangly.com/](https://admin.jiangly.com/) 8 | 9 | 后端接口文档地址[https://api.jiangly.com/docs/](https://api.jiangly.com/docs/) 10 | 11 | ### 项目说明 12 | 13 | 项目采用前后端分离开发,并且前端分为两部分开发,一共分为,对外展示博客页、后台管理系统、后端服务三部分组成,三个独立的项目。 14 | 15 | 16 | 17 | ### 项目基础技术栈 18 | 19 | * 前台展示:[ Vue2](https://cn.vuejs.org/)、 [NuxtJs](https://nuxtjs.org/) 、[socket.io](https://socket.io/) 20 | * 后台管理:[vue3](https://v3.cn.vuejs.org/)、[fantastic-admin](https://fantastic-admin.netlify.app/)、[vite](https://vitejs.cn/)、[Echars](https://echarts.baidu.com/) 21 | * 后端服务:[NodeJs](https://nodejs.org/zh-cn/)(版本:14.14.6) 、[NestJs](https://docs.nestjs.cn/)、[Typeorm](https://typeorm.biunav.com/)、[socket.io](https://socket.io/)、[Mysql]()、 22 | 23 | 上面是用到的一些基础技术栈,注意后端的包管理器为**pnpm**,其包管理器可能导致依赖安装失败、因为项目很多技术栈我也是为了练手也是第一次使用,很多地方可能已经有些落后了,遇到分歧请参考官方文档 24 | 25 | 26 | 27 | ### 迁移准备 28 | 29 | 如果你想完整的迁移我得博客并进行二次开发,你需要准备如下 30 | 31 | * 基本的云服务器 32 | * Mysql数据库 33 | * 存储目前使用的是腾讯云对象存储cos 34 | * Redis[可选] 35 | 36 | 目前项目里面已经内置了一个测试数据库,和一个免费的资源存储接口,资源上传接口的服务器大概八月份就到期了,建议提前使用自己服务存储,内置的数据库可以直接运行项目就可以启动,只有少部分依赖于cos的对象存储可能无法使用,上传图片类的东西。测试数据库是共享的,希望大家不要天天删库。。。 37 | 38 | 39 | 40 | ### 项目运行 41 | 42 | * 前台项目【Nine-blog-web】 43 | * cnpm install 44 | * npm run dev 45 | * 后端项目【Nine-blog-api】 46 | * pnpm install 47 | * npm run start:dev 48 | * 后台管理【Nine-blog-admin】 49 | * cnpm install 50 | * npm run dev 51 | 52 | 53 | 54 | ### 项目配置 55 | 56 | 大多数的配置都写在了配置文件,部分可能不常用的遗漏掉了,全局搜索**jiangly**包含这个域名的大概率是您你需要替换的东西,其他的随意。 57 | 58 | 59 | 60 | ### 二次开发 61 | 62 | 项目支持你随意二次开发,有问题可以issue或者添加我的**vx**、拉你进讨论群大家一起交流。 63 | 64 | 65 | 66 | ### 博客页面展示 67 | 68 | 69 | 70 | ##### 博客首页 71 | 72 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652779109683image.png) 73 | 74 | 75 | 76 | ##### 文章详情页 77 | 78 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652780451187image.png) 79 | 80 | 81 | 82 | ##### 工具使用页 83 | 84 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652780609473image.png) 85 | 86 | 87 | 88 | ##### 博客工具箱页面 89 | 90 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652779257607image.png) 91 | 92 | 93 | 94 | ##### 博客作品页 95 | 96 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652779231586image.png) 97 | 98 | 99 | 100 | ##### 博客公共音乐聊天室 101 | 102 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652779472688image.png) 103 | 104 | 105 | 106 | ##### 后台管理首页 107 | 108 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652780039693image.png) 109 | 110 | 111 | 112 | ##### 后台博客管理 113 | 114 | ![](https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1652780093194image.png) 115 | 116 | 117 | 118 | * 更多细节等你来提...... 119 | 120 | 121 | 122 | * B站视频分享: [查看视频,期待三连](https://www.bilibili.com/video/BV1ZS4y1B7SH/) 123 | 124 | -------------------------------------------------------------------------------- /env_template: -------------------------------------------------------------------------------- 1 | 2 | # 生产环境模板 3 | NODE_ENV=******** 4 | 5 | DB_ALL_STAR_BASE_HOST=******** 6 | DB_ALL_STAR_BASE_PORT=******** 7 | DB_ALL_STAR_BASE_NAME=******** 8 | DB_ALL_STAR_BASE_USERNAME=******** 9 | DB_ALL_STAR_BASE_PASSWORD=******** 10 | 11 | DB_ECO_ADMIN_HOST=******** 12 | DB_ECO_ADMIN_NAME=******** 13 | DB_ECO_ADMIN_USERNAME=******** 14 | DB_ECO_ADMIN_PASSWORD=******** 15 | 16 | DB_MATCH_ADMIN_HOST=******** 17 | DB_MATCH_ADMIN_NAME=******** 18 | DB_MATCH_ADMIN_USERNAME=******** 19 | DB_MATCH_ADMIN_PASSWORD=******** 20 | 21 | #DB_HOST=******** 22 | #DB_DBNAME=******** 23 | #DB_USERNAME=******** 24 | #DB_PASSWORD=******** 25 | 26 | JWT_SECRET=******** 27 | APP_USER_BASE=******** 28 | AUTH_BASE=******** 29 | GAME_DATA_BASE=******** 30 | MATCH_DATA_BASE=******** 31 | APP_SMS_BASE=******** 32 | 33 | REDIS_HOST=******** 34 | REDIS_PORT=******** 35 | REDIS_PASS=******** 36 | REDIS_NEEDPASS=******** 37 | 38 | TENTCENT_SECRET_ID=******** 39 | TENTCENT_SECRET_KEY=******** 40 | 41 | COS_BUCKET_PUBLIC=******** 42 | COS_BUCKET_PRIVATE=******** 43 | COS_BUCKET_PRIVATE_CDN=******** 44 | COS_BUCKET_STATIC=******** 45 | COS_REGION=******** 46 | 47 | #domain production 48 | DOMAIN_PRODUCTION=******** 49 | #domain 50 | DOMAIN=******** 51 | #Protocol 52 | PROTOCOL=******** 53 | #directory>>>static 54 | DIR_STATIC_IMAGES=******** 55 | 56 | #pageSize 57 | DB_PAGE_SIZE =******** 58 | 59 | #max invite count 60 | MAX_INVITE_COUNT=******** 61 | 62 | #smtp 63 | SMTP_HOST=******** 64 | SMTP_FROM=******** 65 | SMTP_USER=******** 66 | SMTP_PASS=******** 67 | 68 | #coinbase 69 | COINBASE_KEY=******** 70 | COINBASE_SECRET=******** 71 | 72 | #aws 73 | AWS_ACCESS_KEY_ID=******** 74 | AWS_SECRET_ACCESS_KEY=******** 75 | 76 | #aliyun 77 | ALIYUN_KEY=******** 78 | ALIYUN_SECRET=******** 79 | 80 | #recaptcha 81 | RECAPTCHA_KEY=******** 82 | RECAPTCHA_SECRET=******** 83 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["templates/**/*","views/**/*"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snine-blog-backend", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "cross-env NODE_ENV=development nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "cross-env NODE_ENV=production node dist/main.js", 16 | "pro": "cross-env NODE_ENV=production pm2 start pm2.conf.json --no-daemon", 17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:cov": "jest --coverage", 21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 22 | "test:e2e": "jest --config ./test/jest-e2e.json" 23 | }, 24 | "dependencies": { 25 | "@nestjs/cli": "^8.0.0", 26 | "@nestjs/common": "^8.0.0", 27 | "@nestjs/core": "^8.0.0", 28 | "@nestjs/jwt": "^8.0.0", 29 | "@nestjs/passport": "^8.0.1", 30 | "@nestjs/platform-express": "^8.0.0", 31 | "@nestjs/platform-ws": "^8.1.1", 32 | "@nestjs/schematics": "^8.0.0", 33 | "@nestjs/swagger": "^5.0.9", 34 | "@nestjs/testing": "^8.0.0", 35 | "@nestjs/typeorm": "^8.0.2", 36 | "@types/express": "^4.17.13", 37 | "@types/jest": "^27.0.1", 38 | "@types/node": "^16.0.0", 39 | "@types/passport-jwt": "^3.0.6", 40 | "@types/supertest": "^2.0.11", 41 | "@typescript-eslint/eslint-plugin": "^4.28.2", 42 | "@typescript-eslint/parser": "^4.28.2", 43 | "bcryptjs": "^2.4.3", 44 | "class-transformer": "^0.4.0", 45 | "class-validator": "^0.13.1", 46 | "cross-env": "^7.0.3", 47 | "dotenv": "^10.0.0", 48 | "eslint": "^7.30.0", 49 | "eslint-config-prettier": "^8.3.0", 50 | "eslint-plugin-prettier": "^3.4.0", 51 | "jest": "^27.0.6", 52 | "moment": "^2.29.1", 53 | "nestjs-config": "^1.4.8", 54 | "npm": "^7.22.0", 55 | "passport": "^0.4.1", 56 | "passport-jwt": "^4.0.0", 57 | "passport-local": "^1.0.0", 58 | "prettier": "^2.3.2", 59 | "reflect-metadata": "^0.1.13", 60 | "rimraf": "^3.0.2", 61 | "rxjs": "^7.2.0", 62 | "socket.io-redis": "^6.1.1", 63 | "supertest": "^6.1.3", 64 | "swagger-ui-express": "^4.1.6", 65 | "ts-jest": "^27.0.3", 66 | "ts-loader": "^9.2.3", 67 | "ts-node": "^10.0.0", 68 | "tsconfig-paths": "^3.10.1", 69 | "typeorm": "^0.2.37", 70 | "typescript": "^4.3.5" 71 | }, 72 | "devDependencies": { 73 | "@nest-modules/mailer": "^1.3.22", 74 | "@nestjs-modules/mailer": "^1.6.0", 75 | "@nestjs/cli": "^8.0.0", 76 | "@nestjs/jwt": "^8.0.0", 77 | "@nestjs/common": "^8.0.0", 78 | "@nestjs/passport": "^8.0.1", 79 | "@nestjs/platform-express": "^8.2.0", 80 | "@nestjs/platform-socket.io": "^8.1.1", 81 | "@nestjs/schedule": "^1.0.1", 82 | "@nestjs/schematics": "^8.0.0", 83 | "@nestjs/testing": "^8.0.0", 84 | "@nestjs/typeorm": "^8.0.2", 85 | "@nestjs/websockets": "^8.1.1", 86 | "@types/express": "^4.17.13", 87 | "@types/jest": "^27.0.1", 88 | "@types/node": "^16.0.0", 89 | "@types/passport-jwt": "^3.0.6", 90 | "@types/socket.io": "^3.0.2", 91 | "@types/supertest": "^2.0.11", 92 | "@typescript-eslint/eslint-plugin": "^4.28.2", 93 | "@typescript-eslint/parser": "^4.28.2", 94 | "axios": "^0.24.0", 95 | "bcryptjs": "^2.4.3", 96 | "cheerio": "^1.0.0-rc.10", 97 | "class-transformer": "^0.4.0", 98 | "class-validator": "^0.13.1", 99 | "cos-nodejs-sdk-v5": "^2.10.6", 100 | "cross-env": "^7.0.3", 101 | "dotenv": "^10.0.0", 102 | "ejs": "^3.1.6", 103 | "eslint": "^7.30.0", 104 | "eslint-config-prettier": "^8.3.0", 105 | "eslint-plugin-prettier": "^3.4.0", 106 | "html-to-md": "^0.5.0", 107 | "jest": "^27.0.6", 108 | "moment": "^2.29.1", 109 | "mysql": "^2.18.1", 110 | "nest-status-monitor": "^0.1.4", 111 | "nestjs-config": "^1.4.8", 112 | "nestjs-redis": "^1.3.3", 113 | "nodemailer": "^6.7.0", 114 | "passport": "^0.4.1", 115 | "passport-jwt": "^4.0.0", 116 | "passport-local": "^1.0.0", 117 | "prettier": "^2.3.2", 118 | "request-ip": "^2.1.3", 119 | "socket.io": "^4.3.1", 120 | "supertest": "^6.1.3", 121 | "ts-jest": "^27.0.3", 122 | "ts-loader": "^9.2.3", 123 | "ts-node": "^10.0.0", 124 | "tsconfig-paths": "^3.10.1", 125 | "typeorm": "^0.2.37", 126 | "typescript": "^4.3.5", 127 | "ws": "^8.2.3" 128 | }, 129 | "jest": { 130 | "moduleFileExtensions": [ 131 | "js", 132 | "json", 133 | "ts" 134 | ], 135 | "rootDir": "src", 136 | "testRegex": ".*\\.spec\\.ts$", 137 | "transform": { 138 | "^.+\\.(t|j)s$": "ts-jest" 139 | }, 140 | "collectCoverageFrom": [ 141 | "**/*.(t|j)s" 142 | ], 143 | "coverageDirectory": "../coverage", 144 | "testEnvironment": "node" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pm2.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": { 3 | "name": "blog", 4 | "script": "./dist/main.js", 5 | "watch": true, 6 | "ignore_watch": [ 7 | "node_modules", 8 | "logs" 9 | ], 10 | "instances": 1, 11 | "error_file": "logs/err.log", 12 | "out_file": "logs/out.log", 13 | "log_date_format": "YYYY-MM-DD HH:mm:ss" 14 | } 15 | } -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { EmailModule } from './modules/email/email.module'; 2 | import { TasksModule } from './tasks/tasks.module'; 3 | import { Module } from '@nestjs/common'; 4 | import { UserModule } from './modules/user/user.module'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { ConfigModule, ConfigService } from 'nestjs-config'; 7 | import { resolve } from 'path'; 8 | import { TypeModule } from './modules/type/type.module'; 9 | import { TagModule } from './modules/tag/tag.module'; 10 | import { ArticleModule } from './modules/article/article.module'; 11 | import { SpiderModule } from './modules/spider/spider.module'; 12 | import { ResourceTypeModule } from './modules/resource-type/resource-type.module'; 13 | import { ResourceModule } from './modules/resource/resource.module'; 14 | import { CommentModule } from './modules/comment/comment.module'; 15 | import { FriendLinksModule } from './modules/friend-links/friend-links.module'; 16 | import { ProjectModule } from './modules/project/project.module'; 17 | // import { WsChatGateway } from './modules/chat/chat.getaway'; 18 | import { ScheduleModule } from '@nestjs/schedule'; 19 | import { MailerModule } from '@nest-modules/mailer'; 20 | // import statusMonitorConfig from './config/statusMonitor'; 21 | // import { StatusMonitorModule } from 'nest-status-monitor'; 22 | import { RedisModule } from 'nestjs-redis'; 23 | import { CacheService } from './cache/cache.service'; 24 | import { MusicModule } from './modules/music/music.module'; 25 | import { ChatModule } from './modules/chat/chat.module'; 26 | import { UploadModule } from './modules/upload/upload.module'; 27 | import { VerifyModule } from './modules/verify/verify.module'; 28 | import { StatisticsModule } from './modules/statistics/statistics.module'; 29 | import { ToolsModule } from './modules/tools/tools.module'; 30 | import { ToolsTypeModule } from './modules/tools-type/tools-type.module'; 31 | import { CollectModule } from './modules/collect/collect.module'; 32 | 33 | @Module({ 34 | imports: [ 35 | // StatusMonitorModule.setUp(statusMonitorConfig), 36 | ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')), 37 | TypeOrmModule.forRootAsync({ 38 | useFactory: (config: ConfigService) => config.get('database'), 39 | inject: [ConfigService], 40 | }), 41 | MailerModule.forRootAsync({ 42 | useFactory: (config: ConfigService) => config.get('email'), 43 | inject: [ConfigService], 44 | }), 45 | // RedisModule.forRootAsync({ 46 | // useFactory: (configService: ConfigService) => configService.get('redis'), // or use async method 47 | // inject:[ConfigService] 48 | // }), 49 | ScheduleModule.forRoot(), 50 | UserModule, 51 | TypeModule, 52 | TagModule, 53 | SpiderModule, 54 | ArticleModule, 55 | ResourceTypeModule, 56 | ResourceModule, 57 | CommentModule, 58 | FriendLinksModule, 59 | ProjectModule, 60 | TasksModule, 61 | EmailModule, 62 | MusicModule, 63 | ChatModule, 64 | UploadModule, 65 | VerifyModule, 66 | StatisticsModule, 67 | ToolsModule, 68 | ToolsTypeModule, 69 | CollectModule, 70 | ], 71 | providers: [], 72 | }) 73 | export class AppModule {} 74 | -------------------------------------------------------------------------------- /src/cache/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { RedisService } from 'nestjs-redis'; 3 | @Injectable() 4 | export class CacheService { 5 | public client; 6 | constructor(private redisService: RedisService) { 7 | this.getClient(); 8 | } 9 | async getClient() { 10 | this.client = await this.redisService.getClient(); 11 | } 12 | 13 | //设置值的方法 14 | async set(key: string, value: any, seconds?: number) { 15 | value = JSON.stringify(value); 16 | if (!this.client) { 17 | await this.getClient(); 18 | } 19 | if (!seconds) { 20 | await this.client.set(key, value); 21 | } else { 22 | await this.client.set(key, value, 'EX', seconds); 23 | } 24 | } 25 | 26 | //获取值的方法 27 | async get(key: string) { 28 | if (!this.client) { 29 | await this.getClient(); 30 | } 31 | const data = await this.client.get(key); 32 | if (!data) return; 33 | return JSON.parse(data); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/common/entity/baseEntity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @CreateDateColumn({ 9 | type: 'datetime', 10 | length: 0, 11 | nullable: false, 12 | name: 'createdAt', 13 | comment: '创建时间', 14 | }) 15 | createdAt: Date; 16 | 17 | @UpdateDateColumn({ 18 | type: 'datetime', 19 | length: 0, 20 | nullable: false, 21 | name: 'updatedAt', 22 | comment: '更新时间', 23 | }) 24 | updatedAt: Date; 25 | 26 | @DeleteDateColumn({ 27 | type: 'datetime', 28 | length: 0, 29 | nullable: false, 30 | name: 'deletedAt', 31 | comment: '删除时间', 32 | }) 33 | deletedAt: Date; 34 | } 35 | -------------------------------------------------------------------------------- /src/config/database.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { ConnectionOptions } from 'typeorm'; 3 | 4 | const developmentConfig: ConnectionOptions = { 5 | type: 'mysql', 6 | port: Number(process.env.DB_PORT), 7 | host: process.env.DB_HOST_DEV, 8 | username: process.env.USERNAEM_DEV, 9 | password: process.env.PASSWORD_DEV, 10 | database: process.env.DB_DATABASE_DEV, 11 | entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')], 12 | logging: false, 13 | synchronize: true, 14 | }; 15 | 16 | const productionConfig: ConnectionOptions = { 17 | type: 'mysql', 18 | port: Number(process.env.DB_PORT), 19 | host: process.env.DB_HOST_PRO, 20 | username: process.env.USERNAEM_PRO, 21 | password: process.env.PASSWORD_PRO, 22 | database: process.env.DB_DATABASE_PRO, 23 | entities: [join(__dirname, '../', '**/**.entity{.ts,.js}')], 24 | logging: false, 25 | synchronize: true, 26 | }; 27 | 28 | const config = process.env.NODE_ENV == 'production' ? productionConfig : developmentConfig; 29 | 30 | export default config; 31 | -------------------------------------------------------------------------------- /src/config/email.ts: -------------------------------------------------------------------------------- 1 | import { PugAdapter } from '@nestjs-modules/mailer/dist/adapters/pug.adapter'; 2 | import { join } from 'path'; 3 | 4 | /* 暂时不抽离 */ 5 | export default { 6 | transport: { 7 | host: 'smtp.163.com', 8 | port: '465', 9 | auth: { 10 | user: 'Nine_Team@163.com', 11 | pass: 'PWFJMLSQVOBMQCQJ', 12 | }, 13 | }, 14 | from: 'Nine_Team@163.com', 15 | template: { 16 | dir: join(__dirname, '../templates/email/'), 17 | adapter: new PugAdapter(), 18 | options: { 19 | strict: true, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/config/jwt.ts: -------------------------------------------------------------------------------- 1 | import * as Dotenv from 'dotenv'; 2 | Dotenv.config({ path: '.env' }); 3 | 4 | export const secret = process.env.JWT_SECRET; 5 | export const expiresIn = process.env.JWT_EXPIRESIN; 6 | 7 | /* 目前权限相对简单,这里配置接口白名单,表示这些接口不是管理员也可以访问 */ 8 | export const whiteList = ['/api/user/login', '/api/user/register', '/api/upload/file']; 9 | export const postWhiteList = [ 10 | '/api/comment/set', 11 | '/api/user/update', 12 | '/api/chat/updateRoom', 13 | '/api/chat/createRoom', 14 | 'api/collect/set', 15 | ]; 16 | -------------------------------------------------------------------------------- /src/config/redis.ts: -------------------------------------------------------------------------------- 1 | import { RedisModuleOptions } from 'nestjs-redis'; 2 | import * as Dotenv from 'dotenv'; 3 | Dotenv.config({ path: '.env' }); 4 | 5 | /* 暂时先不使用redis */ 6 | const redisOptions = { 7 | port: process.env.DEV_REDIS_PORT, 8 | host: process.env.DEV_REDIS_HOST, 9 | name: process.env.PRO_REDIS_USER, 10 | password: process.env.DEV_REDIS_PASS, 11 | db: 0, 12 | }; 13 | export default redisOptions; 14 | -------------------------------------------------------------------------------- /src/config/statusMonitor.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | pageTitle: 'Nest.js Monitoring Page', 3 | port: 3000, 4 | path: '/aaaa', 5 | ignoreStartsWith: '/healt/alive', 6 | spans: [ 7 | { 8 | interval: 1, // Every second 9 | retention: 60, // Keep 60 datapoints in memory 10 | }, 11 | { 12 | interval: 5, // Every 5 seconds 13 | retention: 60, 14 | }, 15 | { 16 | interval: 15, // Every 15 seconds 17 | retention: 60, 18 | }, 19 | ], 20 | chartVisibility: { 21 | cpu: true, 22 | mem: true, 23 | load: true, 24 | responseTime: true, 25 | rps: true, 26 | statusCodes: true, 27 | }, 28 | healthChecks: [], 29 | }; 30 | -------------------------------------------------------------------------------- /src/config/upload.ts: -------------------------------------------------------------------------------- 1 | import * as Dotenv from 'dotenv'; 2 | Dotenv.config({ path: '.env' }); 3 | 4 | const developmentUploadConfig = { 5 | SecretId: process.env.DEV_TENTCENT_SECRET_ID, 6 | SecretKey: process.env.DEV_TENTCENT_SECRET_KEY, 7 | Bucket: process.env.DEV_COS_BUCKET_PUBLIC, 8 | Region: process.env.DEV_COS_REGION, 9 | }; 10 | 11 | const productionUploadConfig = { 12 | SecretId: process.env.PRO_TENTCENT_SECRET_ID, 13 | SecretKey: process.env.PRO_TENTCENT_SECRET_KEY, 14 | Bucket: process.env.PRO_COS_BUCKET_PUBLIC, 15 | Region: process.env.PRO_COS_REGION, 16 | }; 17 | 18 | const config = process.env.NODE_ENV == 'production' ? developmentUploadConfig : productionUploadConfig; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /src/constant/avatar.ts: -------------------------------------------------------------------------------- 1 | const avatarImages = [ 2 | 'https://img0.baidu.com/it/u=2064236049,533493186&fm=26&fmt=auto', 3 | 'https://img1.baidu.com/it/u=1897719880,2867606276&fm=26&fmt=auto', 4 | 'https://img1.baidu.com/it/u=4257022723,1357471486&fm=26&fmt=auto', 5 | 'https://img2.baidu.com/it/u=3770631990,1502164932&fm=26&fmt=auto', 6 | 'https://img1.baidu.com/it/u=1235679188,872295587&fm=26&fmt=auto', 7 | 'https://img0.baidu.com/it/u=3413948841,2661870664&fm=26&fmt=auto', 8 | 'https://img0.baidu.com/it/u=4118467725,2171033744&fm=26&fmt=auto', 9 | 'https://img0.baidu.com/it/u=1670526758,2978193872&fm=26&fmt=auto', 10 | 'https://img2.baidu.com/it/u=3136876758,3479953824&fm=26&fmt=auto', 11 | 'https://img2.baidu.com/it/u=3010850469,2813118839&fm=26&fmt=auto', 12 | 'https://img2.baidu.com/it/u=2285567582,1185119578&fm=26&fmt=auto', 13 | 'https://img0.baidu.com/it/u=3452625090,3453768659&fm=26&fmt=auto', 14 | 'https://img0.baidu.com/it/u=1800114517,4068633526&fm=26&fmt=auto', 15 | 'https://img1.baidu.com/it/u=1220513416,3900720637&fm=26&fmt=auto', 16 | 'https://img0.baidu.com/it/u=1668941702,4253247057&fm=26&fmt=auto', 17 | 'https://img0.baidu.com/it/u=2083621347,2276995712&fm=26&fmt=auto', 18 | 'https://img2.baidu.com/it/u=2157983744,1504765592&fm=26&fmt=auto', 19 | 'https://img0.baidu.com/it/u=1355225337,195579012&fm=26&fmt=auto', 20 | 'https://img1.baidu.com/it/u=2775059443,2185860664&fm=26&fmt=auto', 21 | 'https://img2.baidu.com/it/u=202823690,3290026690&fm=26&fmt=auto', 22 | 'https://img2.baidu.com/it/u=358349854,3952534042&fm=26&fmt=auto', 23 | 'https://img0.baidu.com/it/u=102394303,3578548733&fm=26&fmt=auto', 24 | 'https://img1.baidu.com/it/u=4092157189,2476043363&fm=26&fmt=auto', 25 | 'https://img1.baidu.com/it/u=1053673692,3714938453&fm=26&fmt=auto&gp=0.jpg', 26 | 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.duoziwang.com%2F2018%2F05%2F20171231191240.gif&refer=http%3A%2F%2Fimg.duoziwang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1635170571&t=66f63e1883cc38e91907f3c6fb7327a3', 27 | ]; 28 | 29 | export const getRandomIntInclusive = (min, max) => { 30 | min = Math.ceil(min); 31 | max = Math.floor(max); 32 | return Math.floor(Math.random() * (max - min + 1)) + min; 33 | }; 34 | 35 | export const randomAvatar = () => { 36 | const index = getRandomIntInclusive(0, avatarImages.length - 1); 37 | return avatarImages[index]; 38 | }; 39 | -------------------------------------------------------------------------------- /src/constant/collec.ts: -------------------------------------------------------------------------------- 1 | export const collectTypeMap = { 2 | mid: 1, 3 | articleId: 2, 4 | toolId: 3, 5 | resourceId: 4, 6 | projectId: 5, 7 | }; 8 | 9 | export const collectModuleMap = { 10 | articleId: 'ArticleModule', 11 | }; 12 | -------------------------------------------------------------------------------- /src/decorator/ipAddress.guard.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | import * as requestIp from 'request-ip'; 4 | import { https_get } from '../utils/tools'; 5 | 6 | export const IpAddress = createParamDecorator(async (data: unknown, ctx: ExecutionContext) => { 7 | const req = ctx.switchToHttp().getRequest(); 8 | let ip = req.clientIp || requestIp.getClientIp(req); 9 | /* TODO 待解决 docker部署中暂时没拿到真实ip 先从客户端上传 */ 10 | if (ip.substr(0, 7) == '::ffff:') { 11 | ip = ip.substr(7); 12 | } 13 | 14 | console.log(ip, '最后拿到的ip'); 15 | // ip='139.226.99.52' 16 | const url = `https://sp0.baidu.com/8aQDcjqpAAV3otqbppnN2DJv/api.php?query=%22+${ip}+%22&co=&resource_id=6006&t=1555898284898&ie=utf8&oe=utf8&format=json&tn=baidu`; 17 | try { 18 | const res: any = await https_get(url); 19 | return res; 20 | } catch (error) { 21 | return { ip: null, address: null }; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/filters/http-exception.filter.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpExceptionFilter } from './http-exception.filter'; 2 | 3 | describe('HttpExceptionFilter', () => { 4 | it('should be defined', () => { 5 | expect(new HttpExceptionFilter()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common'; 2 | import { formatDate } from 'src/utils'; 3 | 4 | @Catch(HttpException) 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | catch(exception: HttpException, host: ArgumentsHost) { 7 | const ctx = host.switchToHttp(); 8 | const response = ctx.getResponse(); 9 | const request = ctx.getRequest(); 10 | const {} = response; 11 | const exceptionRes: any = exception.getResponse(); 12 | 13 | /* 正常情况是个对象,为了简写可以只传入一个字符串错误即可 */ 14 | const message = exceptionRes.constructor === Object ? exceptionRes['message'] : exceptionRes; 15 | // const { message } = exceptionRes; 16 | const statusCode = exception.getStatus() || 400; 17 | /* 是数组就返回错误里的第一条即可,不是就返回字符串 */ 18 | const errorResponse = { 19 | message: Array.isArray(message) ? message[0] : message, 20 | code: statusCode, 21 | success: false, 22 | url: request.originalUrl, 23 | timestamp: new Date().toLocaleDateString(), 24 | }; 25 | const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; 26 | 27 | Logger.error( 28 | `【${formatDate(Date.now())}】${request.method} ${request.url}`, 29 | JSON.stringify(errorResponse), 30 | 'HttpExceptionFilter', 31 | ); 32 | console.log(errorResponse, 'errorResponse'); 33 | 34 | /* 设置返回的状态码、请求头、发送错误信息 */ 35 | response.status(status); 36 | response.header('Content-Type', 'application/json; charset=utf-8'); 37 | response.send(errorResponse); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/guard/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthGuard } from './auth.guard'; 2 | 3 | describe('AuthGuard', () => { 4 | it('should be defined', () => { 5 | expect(new AuthGuard()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/guard/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, HttpException, Injectable, HttpStatus } from '@nestjs/common'; 2 | import * as jwt from 'jsonwebtoken'; 3 | import { secret, whiteList, postWhiteList } from 'src/config/jwt'; 4 | 5 | @Injectable() 6 | export class AuthGuard implements CanActivate { 7 | async canActivate(context: ExecutionContext): Promise { 8 | const request = context.switchToHttp().getRequest(); 9 | const { headers, path, route } = context.switchToRpc().getData(); 10 | 11 | // whiteList 12 | if (whiteList.includes(path)) { 13 | return true; 14 | } 15 | const isGet = route.methods.get; 16 | const token = headers.token || request.headers.token; 17 | 18 | if (token) { 19 | const payload = await this.verifyToken(token, secret); 20 | const { role } = payload; 21 | request.payload = payload; 22 | if (isGet) { 23 | return true; 24 | } else { 25 | if (role == 'admin') { 26 | return true; 27 | } else { 28 | if (postWhiteList.includes(path)) { 29 | return true; 30 | } 31 | throw new HttpException('无权此操作,请联系管理员!!!', HttpStatus.FORBIDDEN); 32 | } 33 | } 34 | } else { 35 | if (isGet) { 36 | return true; 37 | } else { 38 | // return true; 39 | throw new HttpException('你还没登录,请先登录', HttpStatus.UNAUTHORIZED); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * @desc 全局校验token 46 | * @param token 47 | * @param secret 48 | * @returns 49 | */ 50 | private verifyToken(token: string, secret: string): Promise { 51 | return new Promise((resolve) => { 52 | jwt.verify(token, secret, (error, payload) => { 53 | if (error) { 54 | throw new HttpException('身份验证失败', HttpStatus.UNAUTHORIZED); 55 | } else { 56 | resolve(payload); 57 | } 58 | }); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/interceptor/transform.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TransformInterceptor } from './transform.interceptor'; 2 | 3 | describe('TransformInterceptor', () => { 4 | it('should be defined', () => { 5 | expect(new TransformInterceptor()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/interceptor/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, CallHandler, ExecutionContext } from '@nestjs/common'; 2 | import { map } from 'rxjs/operators'; 3 | import { Observable } from 'rxjs'; 4 | interface Response { 5 | data: T; 6 | } 7 | @Injectable() 8 | export class TransformInterceptor implements NestInterceptor> { 9 | intercept(context: ExecutionContext, next: CallHandler): Observable> { 10 | return next.handle().pipe( 11 | map((data) => { 12 | return { 13 | data, 14 | code: 200, 15 | success: true, 16 | message: '请求成功', 17 | }; 18 | }), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/file.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import * as COS from 'cos-nodejs-sdk-v5'; 3 | import config from '../config/upload'; 4 | const { SecretId, SecretKey, Bucket, Region } = config; 5 | 6 | const MIMEType = { 7 | 'image/jpeg': 'jpeg', 8 | 'image/png': 'png', 9 | 'image/svg+xml': 'svg', 10 | 'image/webp': 'webp', 11 | 'image/gif': 'gif', 12 | 'application/pdf': 'pdf', 13 | 'application/msword': 'doc', 14 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 15 | 'application/vnd.ms-excel': 'xls', 16 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', 17 | }; 18 | 19 | const cos = new COS({ 20 | SecretId, 21 | SecretKey, 22 | }); 23 | 24 | /** 25 | * @desc 上传文件 26 | * @params filename 文件名 27 | * @params buffer 文件转为buffer格式 28 | */ 29 | export const putFile = async ({ filename, buffer, dir }) => { 30 | return new Promise((resolve, reject) => { 31 | cos.putObject( 32 | { 33 | Bucket, 34 | Region, 35 | Key: `${dir}/${filename}`, 36 | StorageClass: 'STANDARD', 37 | Body: buffer, 38 | // onProgress: function(progressData) { 39 | // console.log(JSON.stringify(progressData),'上传频率'); 40 | // } 41 | }, 42 | function (err, data) { 43 | if (err) { 44 | return reject(err); 45 | } 46 | return resolve(data); 47 | }, 48 | ); 49 | }); 50 | }; 51 | 52 | /** 53 | * 上传文件到cdn 54 | * @param param0 55 | * @returns 56 | */ 57 | export const saveFile = async ({ filename, buffer, dir = 'blog' }) => { 58 | filename = new Date().getTime() + filename; 59 | const res: any = await putFile({ filename, buffer, dir }); 60 | return res.Location.replace(/^(http:\/\/|https:\/\/|\/\/|)(.*)/, 'https://$2'); 61 | }; 62 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestExpressApplication } from '@nestjs/platform-express'; 2 | import { AuthGuard } from './guard/auth.guard'; 3 | import { HttpExceptionFilter } from './filters/http-exception.filter'; 4 | import { TransformInterceptor } from './interceptor/transform.interceptor'; 5 | import { NestFactory } from '@nestjs/core'; 6 | import { AppModule } from './app.module'; 7 | import { createSwagger } from './swagger/index'; 8 | 9 | import * as Dotenv from 'dotenv'; 10 | import { Logger, ValidationPipe } from '@nestjs/common'; 11 | import { IoAdapter } from '@nestjs/platform-socket.io'; 12 | import { join } from 'path'; 13 | 14 | Dotenv.config({ path: '.env' }); 15 | const PORT = process.env.PORT; 16 | const PREFIX = process.env.PREFIX; 17 | 18 | async function bootstrap() { 19 | const app = await NestFactory.create(AppModule); 20 | app.setGlobalPrefix('/api'); 21 | app.useGlobalPipes(new ValidationPipe()); 22 | app.useGlobalInterceptors(new TransformInterceptor()); 23 | app.useGlobalFilters(new HttpExceptionFilter()); 24 | app.useGlobalGuards(new AuthGuard()); 25 | app.useWebSocketAdapter(new IoAdapter(app)); 26 | app.setBaseViewsDir(join(__dirname, '..', 'views')); 27 | app.setViewEngine('ejs'); 28 | 29 | createSwagger(app); 30 | app.enableCors(); 31 | await app.listen(PORT, () => { 32 | Logger.log(`服务已经启动,接口请访问:http://localhost:${PORT}`); 33 | Logger.log(`swagger已经启动,文档请访问:http://localhost:${PORT}${PREFIX}`); 34 | }); 35 | } 36 | bootstrap(); 37 | -------------------------------------------------------------------------------- /src/modules/article/article.controller.ts: -------------------------------------------------------------------------------- 1 | import { ArticleReadDto } from './dto/read.article.dto'; 2 | import { ArticleSetDto } from './dto/set.article.dto'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { ArticleService } from './article.service'; 5 | import { Controller, Post, Get, Body, Query, Request, Response } from '@nestjs/common'; 6 | 7 | @ApiTags('Article') 8 | @Controller('article') 9 | export class ArticleController { 10 | constructor(private readonly articleService: ArticleService) {} 11 | 12 | @Post('/set') 13 | set(@Body() params: ArticleSetDto) { 14 | // set(@Body() params) { 15 | return this.articleService.set(params); 16 | } 17 | 18 | @Get('/query') 19 | query(@Query() params) { 20 | return this.articleService.query(params); 21 | } 22 | 23 | @Post('/del') 24 | del(@Body() params) { 25 | return this.articleService.del(params); 26 | } 27 | 28 | @Get('/detail') 29 | detail(@Query() params, @Request() req) { 30 | return this.articleService.detail(params, req); 31 | } 32 | 33 | @Get('/hot') 34 | hot(@Query() {}) { 35 | return this.articleService.hot(); 36 | } 37 | 38 | @Get('/read') 39 | read(@Query() params: ArticleReadDto) { 40 | return this.articleService.read(params); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modules/article/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { Column, Entity, ManyToOne } from 'typeorm'; 3 | import { BaseEntity } from 'src/common/entity/baseEntity'; 4 | 5 | @Entity({ name: 'tb_article' }) //数据表的名字 6 | export class ArticleEntity extends BaseEntity { 7 | @Column({ length: 32, unique: true, comment: '文章标题' }) 8 | title: string; 9 | 10 | @Column({ length: 300, comment: '文章描述' }) 11 | desc: string; 12 | 13 | @Column({ type: 'text', comment: '文章内容' }) 14 | content: string; 15 | 16 | @Column({ comment: '状态' }) 17 | status: number; 18 | 19 | @Column({ comment: '用户ID' }) 20 | userId: number; 21 | 22 | @Column({ comment: '分类ID' }) 23 | typeId: number; 24 | 25 | @Column({ comment: '标签ID ,相连' }) 26 | tagId: string; 27 | 28 | @Column({ comment: '文章封面图片' }) 29 | coverImg: string; 30 | 31 | @Column({ nullable: true, comment: '排序ID' }) 32 | orderId: number; 33 | 34 | @Column({ length: 300, nullable: true, comment: '背景音乐' }) 35 | bgMusic: string; 36 | 37 | @Column({ nullable: true, default: 0, comment: '阅读量' }) 38 | readVolume: number; 39 | 40 | @Column({ nullable: true, default: 0, comment: '收藏人数' }) 41 | collectionVolume: number; 42 | 43 | @Column({ nullable: true, default: 1, comment: '布局显示方式' }) 44 | layoutMode: number; 45 | 46 | @Column({ nullable: true, default: 1, comment: '是否自动播放背景音乐 1:是 其他:不是' }) 47 | autoPlay: number; 48 | 49 | @ManyToOne(() => UserEntity) 50 | user: UserEntity; 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/article/article.module.ts: -------------------------------------------------------------------------------- 1 | import { CollectEntity } from './../collect/collect.entity'; 2 | import { TagEntity } from './../tag/tag.entity'; 3 | import { TypeEntity } from './../type/type.entity'; 4 | import { UserEntity } from './../user/user.entity'; 5 | import { CommonService } from './../common/common.service'; 6 | import { ArticleEntity } from './article.entity'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { Module } from '@nestjs/common'; 9 | import { ArticleService } from './article.service'; 10 | import { ArticleController } from './article.controller'; 11 | 12 | @Module({ 13 | imports: [TypeOrmModule.forFeature([ArticleEntity, UserEntity, TypeEntity, TagEntity, CollectEntity])], 14 | providers: [ArticleService, CommonService], 15 | controllers: [ArticleController], 16 | }) 17 | export class ArticleModule {} 18 | -------------------------------------------------------------------------------- /src/modules/article/article.service.ts: -------------------------------------------------------------------------------- 1 | import { CollectEntity } from './../collect/collect.entity'; 2 | import { UserEntity } from './../user/user.entity'; 3 | import { CommonService } from './../common/common.service'; 4 | import { Repository, LessThan } from 'typeorm'; 5 | import { ArticleEntity } from './article.entity'; 6 | import { InjectRepository } from '@nestjs/typeorm'; 7 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 8 | import { Like, In } from 'typeorm'; 9 | @Injectable() 10 | export class ArticleService { 11 | constructor( 12 | @InjectRepository(ArticleEntity) 13 | private readonly ArticleModel: Repository, 14 | @InjectRepository(UserEntity) 15 | private readonly UserModel: Repository, 16 | @InjectRepository(CollectEntity) 17 | private readonly CollectModel: Repository, 18 | private readonly CommonService: CommonService, 19 | ) {} 20 | 21 | /** 22 | * @desc 设置新增的时候需要如果没有orderId 需要自己追加 追加规则是 23 | * 1: 大于 5000 的id视为置顶文章 24 | * 2: 每次新增拿到 5000 以下的最大 orderId 为其加上10代表新的一条的id 25 | */ 26 | async set(params) { 27 | const { id, title } = params; 28 | if (id) return this.ArticleModel.update({ id }, params); 29 | const article = await this.ArticleModel.findOne({ title }); 30 | if (article && !id) throw new HttpException('此文章已经上传过了!', HttpStatus.BAD_REQUEST); 31 | /* 添加新文章前查询最大orderId */ 32 | const { orderId = 99 } = await this.ArticleModel.findOne({ 33 | where: { orderId: LessThan(5000) }, 34 | order: { orderId: 'DESC' }, 35 | select: ['id', 'orderId'], 36 | }); 37 | !params.orderId && (params.orderId = orderId + 10); 38 | return this.ArticleModel.save(params); 39 | } 40 | 41 | /** 42 | * @desc 文章查询接口 43 | * 1:tag没有关联表,存的是[].join(',')格式,所以后端查询typeorm没有api支持,所以这里改为sql查询 44 | * 2:查询多标题的时候是or关系需要(),需要注意 45 | * 3:子查询或者left join查询的性能实际很低,自己用代码查询组装数据性能会高很多,只需要查询两次 46 | * @param params 47 | * @returns 48 | */ 49 | async query(params) { 50 | const { page = 1, pageSize = 10, typeId, keyword, status } = params; 51 | let { tagId } = params; 52 | tagId && (tagId = Array.isArray(tagId) ? tagId : [tagId]); // get方法如果只有图个长度就变成字符串了都统一转为数组 53 | /* 子查询只可以查询一个字段 */ 54 | // let sql = 'select *,(select nickname from tb_user where tb_user.id = userId) as nickname,(select name from tb_type where tb_type.id = typeId) as typename from tb_article where id > 0' 55 | /* left join可以查询整表,可以指定字段 */ 56 | // let sql = 'select A.*,B.nickname,B.username from tb_article as A LEFT JOIN tb_user as B on A.userId = B.id where A.id > 0' 57 | /* 基础查询即可,扩展数据自己组装 */ 58 | let sql = 'select * from tb_article as A where A.id > 0'; 59 | if (tagId) { 60 | let tagSql = ''; 61 | tagId.forEach( 62 | (t, i) => (tagSql += i == tagId.length - 1 ? `find_in_set(${t}, A.tagId)` : `find_in_set(${t}, A.tagId) or `), 63 | ); 64 | sql += ` and (${tagSql})`; 65 | } 66 | typeId && (sql += ` and A.typeId = ${typeId}`); 67 | status && (sql += ` and A.status = ${status}`); 68 | keyword && (sql += ` and A.title like '%${keyword}%'`); 69 | sql += ` ORDER BY orderId DESC`; 70 | /* 查询count总数要在分页前查询完 */ 71 | const countSql = ` select count(*) as total from (${sql}) as a`; 72 | const count: any = await this.ArticleModel.query(countSql); 73 | /* 添加分页 */ 74 | sql += ' limit ' + (page - 1) * pageSize + ',' + pageSize; 75 | const rows: any = await this.ArticleModel.query(sql); 76 | const userIds = rows.map((t) => t.userId); 77 | const typeIds = rows.map((t) => t.typeId); 78 | const tagIds = []; 79 | rows.map((t) => t.tagId.split(',')).forEach((k) => k.forEach((j) => tagIds.push(j))); 80 | const [users, types, tags]: any = await Promise.all([ 81 | this.CommonService.findUserMap(userIds), 82 | this.CommonService.findTypeMap(typeIds), 83 | this.CommonService.findTagMap([...new Set(tagIds)]), 84 | ]); 85 | await this.CommonService.mergeArticleInfo({ data: rows, users, types, tags }); 86 | return { rows, count: Number(count[0].total) }; 87 | } 88 | 89 | /** 90 | * @desc 删除文章 91 | * @param params 92 | * @returns 93 | */ 94 | async del(params) { 95 | const { id } = params; 96 | return await this.ArticleModel.delete({ id }); 97 | } 98 | 99 | /** 100 | * @desc 文章详情 101 | */ 102 | async detail(params, req) { 103 | const { id } = params; 104 | const res: any = await this.ArticleModel.findOne({ id }); 105 | const { tagId, userId } = res; 106 | const user: any = await this.UserModel.findOne({ where: { id: userId }, select: ['avatar'] }); 107 | res.avatar = user.avatar; 108 | res.tagArray = await this.CommonService.findTagMap(tagId.split(',')); 109 | if (req?.payload?.userId) { 110 | const isLiked = await this.CollectModel.count({ userId, articleId: id, delete: 0 }); 111 | isLiked && (res.isLiked = true); 112 | } 113 | return res; 114 | } 115 | 116 | /** 117 | * @desc 热门文章 待接入访问量之后使用 118 | * @returns 119 | */ 120 | async hot() { 121 | return await this.ArticleModel.find({ 122 | order: { readVolume: 'DESC' }, 123 | select: ['id', 'title', 'readVolume'], 124 | take: 10, 125 | }); 126 | } 127 | 128 | /** 129 | * @desc 统计文章访问量 130 | * @param param 文章id 131 | */ 132 | async read({ id }) { 133 | const article = await this.ArticleModel.findOne({ where: { id }, select: ['readVolume'] }); 134 | const readVolume = article ? article.readVolume + 1 : 1; 135 | return await this.ArticleModel.update({ id }, { readVolume }); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/modules/article/dto/read.article.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiProperty } from '@nestjs/swagger'; 2 | 3 | export class ArticleReadDto { 4 | @ApiProperty({ example: 1, description: '文章ID' }) 5 | id: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/article/dto/set.article.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsInt, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class ArticleSetDto { 6 | @ApiProperty({ example: '关于xxx的总结', description: '文章标题' }) 7 | @IsNotEmpty({ message: '标题不能为空' }) 8 | title: string; 9 | 10 | @ApiProperty({ example: '这是一篇关于xx的总结,主要讲述了。。。', description: '文章描述' }) 11 | @IsNotEmpty({ message: '描述不能为空' }) 12 | desc: string; 13 | 14 | @ApiProperty({ example: 'http://xxxxx.png', description: '文章封面图片' }) 15 | @IsNotEmpty({ message: '封面图片不能为空' }) 16 | coverImg: string; 17 | 18 | @ApiProperty({ example: 'http://xxxxx.mp3', description: '地址', required: false }) 19 | bgMusic: string; 20 | 21 | @ApiProperty({ example: 1, description: '作者id' }) 22 | @IsInt({ message: '作者Id参数类型错误' }) 23 | userId: number; 24 | 25 | @ApiProperty({ example: 1, description: '排序Id 数字越大越靠前', required: false }) 26 | orderId: number; 27 | 28 | // @ApiProperty({ example: '1,2', description: '标签id,[number]' }) 29 | // @IsInt({ message: '标签id参数类型错误' }) 30 | // tagId: string; 31 | 32 | @ApiProperty({ example: 1, description: '分类id' }) 33 | @IsInt({ message: '分类id参数类型错误' }) 34 | typeId: number; 35 | 36 | @ApiProperty({ example: '关于xxxxx', description: '文章主要内容' }) 37 | content: string; 38 | 39 | @ApiProperty({ 40 | example: 1, 41 | description: '文章状态,1:已发布,2:草稿', 42 | required: true, 43 | enum: [1, -1], 44 | name: 'status', 45 | }) 46 | @IsOptional() 47 | @IsEnum([1, -1], { message: '状态参数非法' }) 48 | @Type(() => Number) 49 | status: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/modules/chat/chat.controller.ts: -------------------------------------------------------------------------------- 1 | import { createUpdateRoomInfoDto } from './dto/room.dto'; 2 | import { ChatService } from './chat.service'; 3 | import { Controller, Get, Post, Query, Body, Req, Request } from '@nestjs/common'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | import { emoticonSearchDto, roomInfoDto } from './dto/search.dto'; 6 | 7 | @ApiTags('Chat') 8 | @Controller('chat') 9 | export class ChatController { 10 | constructor(private readonly ChatService: ChatService) {} 11 | 12 | @Get('/history') 13 | history(@Query() params) { 14 | return this.ChatService.history(params); 15 | } 16 | 17 | @Get('/emoticon') 18 | emoticon(@Query() params: emoticonSearchDto) { 19 | return this.ChatService.emoticon(params); 20 | } 21 | 22 | @Get('/roomInfo') 23 | roomInfo(@Query() params: roomInfoDto) { 24 | return this.ChatService.roomInfo(params); 25 | } 26 | 27 | @Post('/updateRoom') 28 | updateRoom(@Body() params, @Request() req) { 29 | return this.ChatService.updateRoom(params, req.payload); 30 | } 31 | 32 | @Post('/createRoom') 33 | createRoom(@Body() params: createUpdateRoomInfoDto, @Request() req) { 34 | return this.ChatService.createRoom(params, req.payload); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/chat/chat.getaway.ts: -------------------------------------------------------------------------------- 1 | import { CollectEntity } from './../collect/collect.entity'; 2 | import { RoomEntity } from './room.entity'; 3 | import { UserEntity } from './../user/user.entity'; 4 | import { MusicService } from './../music/music.service'; 5 | import { MusicEntity } from './../music/music.entity'; 6 | import { ConnectedSocket, MessageBody, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; 7 | import { Logger } from '@nestjs/common'; 8 | import { Server, Socket } from 'socket.io'; 9 | import { verifyToken } from 'src/utils/verifyToken'; 10 | import { formatOnlineUser } from 'src/utils/tools'; 11 | import { InjectRepository } from '@nestjs/typeorm'; 12 | import { Repository } from 'typeorm'; 13 | import { getRandomIntInclusive } from '../../constant/avatar'; 14 | import { MessageEntity } from './message.entity'; 15 | import { getTimeSpace } from '../../utils/tools'; 16 | /** 17 | * @desc ws 18 | * 1.client.emit('online', 'xxx'); 给当前建立连接的用户广播 19 | * 2.client.broadcast.emit('online', 'xxx'); 给除了当前用户的其他人广播 20 | * 3.this.socket.emit('online', 'xxx'); 给包含当前用户的所有人广播 21 | */ 22 | @WebSocketGateway(3002, { 23 | path: '/chat', 24 | allowEIO3: true, 25 | // cors: true, 26 | cors: { 27 | origin: /.*/, 28 | credentials: true, 29 | }, 30 | }) 31 | export class WsChatGateway { 32 | constructor(private readonly MusicService: MusicService) {} 33 | 34 | private logger: Logger = new Logger('ChatGateway'); 35 | @WebSocketServer() private socket: Server; // socket实例 36 | @InjectRepository(MusicEntity) 37 | private readonly MusicModel: Repository; 38 | @InjectRepository(MessageEntity) 39 | private readonly MessageMode: Repository; 40 | @InjectRepository(CollectEntity) 41 | private readonly CollectMode: Repository; 42 | @InjectRepository(UserEntity) 43 | private readonly UserModel: Repository; 44 | @InjectRepository(RoomEntity) 45 | private readonly RoomModel: Repository; 46 | private clientIdMap: any = {}; // 记录clientId和userId 47 | private onlineUserInfo: any = {}; // 全房间在线用户信息,方便拿到单个用户具体信息 48 | private chooseMusicTimeSpace: any = {}; // 记录每位用户的点歌时间 49 | private maxCustomId = 0; // 歌曲有多少首,取这个区间的随机数拿歌曲 50 | private roomList = {}; // 管理一个所有房间的房间信息 51 | 52 | private init = async () => { 53 | const res = await this.MusicModel.query(`select max(customId) as max from tb_music`); 54 | this.maxCustomId = res[0].max; 55 | }; 56 | 57 | private autoFn = async () => { 58 | try { 59 | await this.inspectorChat(); 60 | // await this.switchMusic(); 61 | } catch (error) { 62 | return this.autoFn(); 63 | } 64 | }; 65 | 66 | /* 初始化websocket init ... */ 67 | async afterInit() { 68 | await this.init(); 69 | this.autoFn(); 70 | } 71 | 72 | /** 73 | * @desc 客户端建立连接后首先验证token是否有效,token校验通过后校验用户是否在线,再记录用户与房间关联的所有信息 74 | * @param client 75 | * @returns 76 | */ 77 | async handleConnection(client: Socket): Promise { 78 | const { token, address, roomId } = client.handshake.query; 79 | const payload = await verifyToken(token); // 可能会校验失败 校验失败的时候拿不到userId 80 | const { userId } = payload; 81 | if (userId === -1 || !token) { 82 | client.emit('authFail', { code: -1, msg: '权限校验失败,请重新登录' }); 83 | return client.disconnect(); 84 | } 85 | const u = await this.UserModel.findOne({ id: userId }); 86 | const { username, nickname, email, role, avatar, sign, roomBg } = u; 87 | const userInfo = { username, nickname, email, role, avatar, sign, roomBg, userId }; 88 | const isHasUser = this.onlineUserInfo[userId]; 89 | /* 如果存在这个用户 先告知老的用户被挤掉 踢掉之前加入的那位 */ 90 | if (isHasUser) { 91 | const { uuid } = isHasUser; 92 | /* 通知新老用户 */ 93 | this.socket.to(uuid).emit('tips', { code: -2, msg: '您的账户在别地登录了,您已被迫下线' }); 94 | client.emit('tips', { code: -1, msg: '您的账户已在别地登录,已为您覆盖登录!' }); 95 | /* 踢掉老的用户 */ 96 | this.socket.in(uuid).disconnectSockets(true); 97 | } 98 | const roomInfo = await this.RoomModel.findOne({ where: { roomId } }); 99 | if (!roomInfo) { 100 | client.emit('tips', { code: -1, msg: '您正在尝试加入一个不存在的房间、非法操作!!!' }); 101 | return client.disconnect(); 102 | } 103 | const onlineRoomIds: any = Object.keys(this.roomList).map((roomId) => Number(roomId)); 104 | if (onlineRoomIds.length > 5 && !onlineRoomIds.includes(Number(roomId))) { 105 | client.emit('disableJoin', { 106 | code: -1, 107 | msg: '由于服务器资源有限,房主限制最多在线五个房间,请耐心等候或加入他人房间吧!', 108 | }); 109 | return client.disconnect(); 110 | } 111 | client.join(roomId); 112 | /* 记录房间信息 */ 113 | !this.roomList[Number(roomId)] && (await this.initBasicRoomInfo(roomId, roomInfo)); 114 | this.roomList[Number(roomId)].onlineUserList.push(userInfo); 115 | /* uuid: 唯一通信id */ 116 | this.onlineUserInfo[userId] = { userInfo, roomId, uuid: client.id }; // 在线信息同时记录房间号 117 | this.clientIdMap[client.id] = userId; // 通过clientId 映射到 userId 118 | this.joinRoomSuccess(client, userId, nickname, address, roomId); 119 | } 120 | 121 | /** 122 | * @desc 断开连接时需要干嘛 123 | * 1. 拿到用户信息和用户所在房间信息 并把该用户从全局在线信息中移除 124 | * 2. 移除此用户,并且下放通过变更当前房间在线用户的在线用户信息 125 | * 3. 检测当前房间剩余人数,如果当前房间人数为0,则删除此房间在线状态 126 | * 如果房间还有人,就再次更新房间在线列表 127 | * @param client 128 | */ 129 | handleDisconnect(client: Socket) { 130 | const userId = this.clientIdMap[client.id]; 131 | const user = this.onlineUserInfo[userId]; 132 | if (!user) return; 133 | const { userInfo, roomId } = user; 134 | const { nickname } = userInfo; 135 | const delUserIndex = this.roomList[Number(roomId)].onlineUserList.findIndex((t) => t.userId === userId); 136 | this.roomList[Number(roomId)].onlineUserList.splice(delUserIndex, 1); 137 | delete this.onlineUserInfo[userId]; 138 | if (!this.roomList[Number(roomId)].onlineUserList.length && Number(roomId) !== 888) { 139 | return delete this.roomList[Number(roomId)]; 140 | } 141 | const onLineUserInfo = formatOnlineUser(this.roomList[Number(roomId)].onlineUserList); 142 | this.socket.to(roomId).emit('offline', { code: 1, data: onLineUserInfo, msg: `${nickname}离开房间了` }); 143 | } 144 | 145 | /* 查询在线人数 */ 146 | @SubscribeMessage('query') 147 | handleQueryOnline(client: Socket, data: any): void { 148 | const onLineUserInfo = formatOnlineUser(this.onlineUserInfo); 149 | client.emit('query', { data: onLineUserInfo, type: data, msg: '查询信息完成' }); 150 | } 151 | 152 | /* 接收到客户端的消息 */ 153 | @SubscribeMessage('message') 154 | async handleMessage(client: Socket, data: any) { 155 | const { type, content } = data; 156 | const userId = this.clientIdMap[client.id]; 157 | const user = this.onlineUserInfo[userId]; 158 | const { userInfo, roomId } = user; 159 | const params = { userId, content, type, roomId }; 160 | const res = await this.MessageMode.save(params); 161 | const { id } = res; 162 | this.socket.to(roomId).emit('message', { 163 | data: { type, content, userId, id, userInfo, createdAt: new Date() }, 164 | msg: '有一条新消息', 165 | }); 166 | } 167 | 168 | /* 用户修改个人信息,db查找新资料更新到在线列表 */ 169 | @SubscribeMessage('updateUserInfo') 170 | async handleUpdateUserInfo(client: Socket, data: any) { 171 | const userId = this.clientIdMap[client.id]; 172 | const user = this.onlineUserInfo[userId]; 173 | const { roomId } = user; 174 | const u = await this.UserModel.findOne({ 175 | where: { id: userId }, 176 | select: ['username', 'nickname', 'email', 'avatar', 'role', 'roomBg', 'sign'], 177 | }); 178 | const { username, nickname, email, avatar, role, roomBg, sign } = u; 179 | this.onlineUserInfo[userId] = { username, nickname, email, avatar, role, roomBg, sign }; 180 | const onlineUserInfo = formatOnlineUser(this.onlineUserInfo); 181 | this.socket.to(roomId).emit('online', { code: 1, data: onlineUserInfo, msg: `${nickname}更新了个人信息` }); 182 | } 183 | 184 | /* 点击收藏音乐 */ 185 | @SubscribeMessage('collectMusic') 186 | async handlerCollectMusic(client: Socket, data: any) { 187 | const musicInfo = data.data; 188 | const { userId, mid } = data.data; 189 | const c = await this.CollectMode.count({ where: { userId, mid } }); 190 | if (c > 0) { 191 | const data = { code: -1, msg: '您已经收藏过这首歌了!' }; 192 | client.emit('collectMusic', data); 193 | } else { 194 | musicInfo.typeId = 1; 195 | await this.CollectMode.save(musicInfo); 196 | const data = { code: 1, msg: '恭喜您收藏成功' }; 197 | client.emit('collectMusic', data); 198 | } 199 | } 200 | 201 | /* 点歌操作 */ 202 | @SubscribeMessage('chooseMusic') 203 | async handlerChooseMusic(client: Socket, musicInfo: any) { 204 | const userId = this.clientIdMap[client.id]; 205 | const user = this.onlineUserInfo[userId]; 206 | const { userInfo, roomId } = user; 207 | const { musicQueueList } = this.roomList[Number(roomId)]; 208 | const { name, artist, mid } = musicInfo; 209 | if (musicQueueList.some((t) => t.mid === mid)) { 210 | return client.emit('tips', { code: -1, msg: '这首歌已经在列表中啦!' }); 211 | } 212 | /* 计算距离上次点歌时间 */ 213 | if (this.chooseMusicTimeSpace[userId]) { 214 | const timeDifference = getTimeSpace(this.chooseMusicTimeSpace[userId]); 215 | /* 点歌权限控制在这里更改, 可以抽离出去 */ 216 | if (timeDifference <= 15 && !['super', 'guest', 'admin'].includes(userInfo.role)) { 217 | return client.emit('tips', { code: -1, msg: `频率过高 请在${15 - timeDifference}秒后重试` }); 218 | } 219 | } 220 | musicInfo.userInfo = userInfo; 221 | musicQueueList.push(musicInfo); 222 | this.chooseMusicTimeSpace[userId] = getTimeSpace(); 223 | client.emit('tips', { code: 1, msg: '恭喜您点歌成功' }); 224 | this.socket.to(roomId).emit('chooseMusic', { 225 | code: 1, 226 | data: musicQueueList, 227 | msg: `${userInfo.nickname} 点了一首 ${name}(${artist})`, 228 | }); 229 | } 230 | 231 | /* 移除已点歌曲 */ 232 | @SubscribeMessage('removeQueueMusic') 233 | async handlerRemoveQueueMusic(client: Socket, data: any) { 234 | const userId = this.clientIdMap[client.id]; 235 | const { mid, name, artist, userInfo } = data; 236 | if (userId !== userInfo.userId) { 237 | return client.emit('tips', { code: -1, msg: '只能移除掉自己点的歌曲哟...' }); 238 | } else { 239 | const user = this.onlineUserInfo[userId]; 240 | const { userInfo, roomId } = user; 241 | const { musicQueueList } = this.roomList[Number(roomId)]; 242 | const index = musicQueueList.findIndex((t) => t.mid === mid); 243 | musicQueueList.splice(index, 1); 244 | this.socket.to(roomId).emit('chooseMusic', { 245 | code: 1, 246 | data: musicQueueList, 247 | msg: `${userInfo.nickname} 移除了歌单中的 ${name}(${artist})`, 248 | }); 249 | } 250 | } 251 | 252 | /* 切歌操作 */ 253 | @SubscribeMessage('cutMusic') 254 | async handlerCutMusic(client: Socket, data: any) { 255 | const userId = this.clientIdMap[client.id]; 256 | const user = this.onlineUserInfo[userId]; 257 | const { userInfo, roomId } = user; 258 | const { role, nickname } = userInfo; 259 | /* 需要权限逻辑在这里控制 */ 260 | // if (!['super', 'guest', 'admin'].includes(role)) { 261 | // return client.emit('tips', { code: -1, msg: '当前切歌只对管理员开放哟!' }); 262 | // } 263 | client.emit('tips', { code: 1, msg: '当前房间已允许所有人切歌' }); 264 | const { currentMusicInfo } = this.roomList[Number(roomId)]; 265 | const { album, artist } = currentMusicInfo; 266 | await this.sendNotice(roomId, { type: 'info', content: `${nickname} 切掉了 ${album}(${artist})` }); 267 | this.switchMusic(roomId); 268 | } 269 | 270 | /* 撤回消息 */ 271 | @SubscribeMessage('recallMessafe') 272 | async handlerRecallMessafe(client: Socket, data: any) { 273 | const { id, nickname } = data; 274 | const userId = this.clientIdMap[client.id]; 275 | const user = this.onlineUserInfo[userId]; 276 | const { roomId } = user; 277 | const message = await this.MessageMode.findOne({ id, userId }); 278 | if (!message) { 279 | return client.emit('tips', { code: -1, msg: '非法操作,不可移除他人消息!' }); 280 | } 281 | const { createdAt } = message; 282 | const timeSpace = new Date(createdAt).getTime(); 283 | const now = new Date().getTime(); 284 | if (now - timeSpace > 2 * 60 * 1000) { 285 | return client.emit('tips', { code: -1, msg: '只能撤回两分钟内的消息哟!' }); 286 | } 287 | await this.MessageMode.update({ id }, { status: -1 }); 288 | this.socket.to(roomId).emit('recallMessafe', { code: 1, data: id, msg: `${nickname} 撤回了一条消息` }); 289 | // await this.sendNotice({ type: 'info', content: `${nickname} 撤回了一条消息` }); 290 | } 291 | 292 | /* 推荐房间列表 */ 293 | // @SubscribeMessage('recommendRoom') 294 | // async handlerRecommendRoom(client: Socket) { 295 | // const roomList = this.formatRoomList(); 296 | // client.emit('recommendRoom', roomList); 297 | // } 298 | 299 | /* 修改房间信息 */ 300 | @SubscribeMessage('updateRoomInfo') 301 | async handlerUpdateRoomInfo(client: Socket, roomId) { 302 | const roomInfo = await this.RoomModel.findOne({ where: { roomId } }); 303 | delete roomInfo.createdAt; 304 | delete roomInfo.updatedAt; 305 | delete roomInfo.deletedAt; 306 | if (!this.roomList[roomId]) return; 307 | this.roomList[roomId].roomInfo = roomInfo; 308 | const roomList = this.formatRoomList(); 309 | client.emit('recommendRoom', roomList); 310 | } 311 | 312 | /* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< */ 313 | 314 | /* 到点切换音乐 */ 315 | async switchMusic(roomId) { 316 | const { mid, userInfo, currentRoomMusicQueueList } = await this.getNextMusicMid(roomId); 317 | try { 318 | const { musicInfo, lrclist: musicLrc } = await this.MusicService.musicInfo({ mid }); 319 | const musicSrc = await this.MusicService.musicUrl({ mid }); 320 | this.roomList[Number(roomId)].currentMusicInfo = musicInfo; 321 | this.roomList[Number(roomId)].musicLrc = musicLrc; 322 | this.roomList[Number(roomId)].musicSrc = musicSrc; 323 | currentRoomMusicQueueList.shift(); // 移除掉队列的第一首歌 324 | const { album, artist } = musicInfo; 325 | this.socket.to(roomId).emit('switchMusic', { 326 | data: { 327 | musicInfo, 328 | musicSrc, 329 | musicLrc, 330 | queueMusicList: currentRoomMusicQueueList, 331 | }, 332 | msg: `正在播放${userInfo ? userInfo.nickname : '系统随机'}点播的 ${album}(${artist})`, 333 | }); 334 | const { duration } = musicInfo; 335 | this.roomList[Number(roomId)].nextMusicTimespace = new Date().getTime() + duration * 1000; 336 | } catch (error) { 337 | currentRoomMusicQueueList.shift(); // 移除掉队列的第一首歌 338 | this.switchMusic(roomId); 339 | return this.sendNotice(roomId, { type: 'info', content: `当前歌曲加载失败、试试其他歌曲吧!` }); 340 | } 341 | } 342 | 343 | /** 344 | * @desc 告知客户端已经成功加入房间,并返回当前房间具体信息 客户端请求房间基础信息进行整合 345 | * @param userId 用户Id 346 | * @param nickname 用户昵称 347 | * @param roomId 房间Id 348 | */ 349 | joinRoomSuccess(client, userId, nickname, address, roomId) { 350 | const { currentMusicInfo, musicQueueList, musicSrc, musicLrc, onlineUserList, nextMusicTimespace } = 351 | this.roomList[Number(roomId)]; 352 | const { duration } = currentMusicInfo; 353 | const startTime = duration - Math.round((nextMusicTimespace - new Date().getTime()) / 1000); 354 | 355 | const formatUserList = formatOnlineUser(onlineUserList); 356 | client.emit('joinRoomSuccess', { 357 | userId, 358 | musicSrc, 359 | currentMusicInfo, 360 | musicLrc, 361 | musicQueueList, 362 | onlineUserList: formatUserList, 363 | startTime, 364 | msg: `欢迎${nickname}加入房间!`, 365 | }); 366 | /* 新用户上线通知房间其他人 */ 367 | this.socket 368 | .to(roomId) 369 | .emit('newUserOnline', { onlineUserList: formatUserList, msg: `来自${address}的${nickname}进入了房间` }); 370 | const roomList = this.formatRoomList(); 371 | this.socket.emit('recommendRoom', roomList); 372 | } 373 | 374 | /** 375 | * @desc 队列有音乐优先播放队列,队列没有从数据库随机拿一首 376 | * @returns 377 | */ 378 | async getNextMusicMid(roomId) { 379 | let mid: any; 380 | let userInfo: any = null; 381 | let musicQueueList = []; 382 | this.roomList[Number(roomId)] && (musicQueueList = this.roomList[Number(roomId)].musicQueueList); 383 | if (musicQueueList.length) { 384 | mid = musicQueueList[0].mid; 385 | userInfo = musicQueueList[0]?.userInfo; 386 | } else { 387 | const customId = getRandomIntInclusive(1, this.maxCustomId); 388 | const res = await this.MusicModel.findOne({ customId }); 389 | mid = res.mid; 390 | } 391 | return { mid, userInfo, currentRoomMusicQueueList: musicQueueList }; 392 | } 393 | 394 | /** 395 | * @desc 发送给客户端全局通知或者公告 396 | * @param param0 397 | */ 398 | sendNotice(roomId, { type, content }) { 399 | this.socket.to(roomId).emit('notice', { type, content }); 400 | } 401 | 402 | /** 403 | * @desc 初始化单个房间信息 404 | * @param roomId 房间Id 405 | * @returns 如果房间是进入第一个人就初始化房间所需基本信息 406 | */ 407 | async initBasicRoomInfo(roomId, roomInfo) { 408 | delete roomInfo.createdAt; 409 | delete roomInfo.updatedAt; 410 | delete roomInfo.deletedAt; 411 | delete roomInfo.id; 412 | const { roomUserId } = roomInfo; 413 | const roomAdminInfo = await this.UserModel.findOne({ 414 | where: { id: roomUserId }, 415 | select: ['nickname', 'avatar'], 416 | }); 417 | this.roomList[Number(roomId)] = { 418 | onlineUserList: [], 419 | musicQueueList: [], 420 | currentMusicInfo: [], 421 | nextMusicTimespace: null, 422 | musicSrc: null, 423 | musicLrc: null, 424 | [`timer${roomId}`]: null, 425 | roomInfo, 426 | roomAdminInfo, 427 | }; 428 | await this.switchMusic(roomId); 429 | } 430 | 431 | /** 432 | * @desc 重复检测各个房间是否需要切歌了,到时间就切换, 433 | * 初始化的时候执行 434 | */ 435 | async inspectorChat() { 436 | setInterval(() => { 437 | if (!Object.keys(this.roomList).length) return; 438 | Object.keys(this.roomList).forEach((roomId) => { 439 | const { nextMusicTimespace } = this.roomList[roomId]; 440 | const nowTimespace = new Date().getTime(); 441 | nowTimespace >= nextMusicTimespace && this.switchMusic(roomId); 442 | }); 443 | }, 5000); 444 | } 445 | 446 | /** 447 | * @desc 格式化房间列表信息 448 | */ 449 | formatRoomList() { 450 | const cloneData = JSON.parse(JSON.stringify(this.roomList)); 451 | const formatRoomList = []; 452 | Object.keys(cloneData).forEach((roomId) => { 453 | const { onlineUserList, roomAdminInfo, roomInfo } = cloneData[roomId]; 454 | const { roomName, roomNotice, roomNeedPassword, roomLogo } = roomInfo; 455 | formatRoomList.push({ 456 | onlineUserNums: onlineUserList.length, 457 | roomName, 458 | roomNotice, 459 | roomLogo, 460 | roomId, 461 | isNeedPassword: roomNeedPassword, 462 | roomAdminNick: roomAdminInfo.nickname, 463 | }); 464 | }); 465 | return formatRoomList; 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /src/modules/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { RoomEntity } from './room.entity'; 2 | import { CollectEntity } from './../collect/collect.entity'; 3 | import { UserEntity } from './../user/user.entity'; 4 | import { SpiderService } from './../spider/spider.service'; 5 | import { MusicService } from './../music/music.service'; 6 | import { MusicEntity } from './../music/music.entity'; 7 | import { TypeOrmModule } from '@nestjs/typeorm'; 8 | import { WsChatGateway } from './chat.getaway'; 9 | import { Module } from '@nestjs/common'; 10 | import { MessageEntity } from './message.entity'; 11 | import { ChatController } from './chat.controller'; 12 | import { ChatService } from './chat.service'; 13 | 14 | @Module({ 15 | imports: [TypeOrmModule.forFeature([MusicEntity, MessageEntity, UserEntity, CollectEntity, RoomEntity])], 16 | providers: [WsChatGateway, MusicService, SpiderService, ChatService], 17 | controllers: [ChatController], 18 | }) 19 | export class ChatModule { 20 | constructor() {} 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/chat/chat.service.ts: -------------------------------------------------------------------------------- 1 | import { RoomEntity } from './room.entity'; 2 | import { requestHtml } from './../../utils/spider'; 3 | import { UserEntity } from './../user/user.entity'; 4 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { In, Repository } from 'typeorm'; 7 | import { MessageEntity } from './message.entity'; 8 | 9 | @Injectable() 10 | export class ChatService { 11 | constructor( 12 | @InjectRepository(MessageEntity) 13 | private readonly MessageModel: Repository, 14 | @InjectRepository(UserEntity) 15 | private readonly UserModel: Repository, 16 | @InjectRepository(RoomEntity) 17 | private readonly RoomModel: Repository, 18 | ) {} 19 | 20 | async history(params) { 21 | const { page = 1, pagesize = 100, roomId } = params; 22 | const messageInfo = await this.MessageModel.find({ 23 | where: { roomId }, 24 | order: { id: 'DESC' }, 25 | skip: (page - 1) * pagesize, 26 | take: pagesize, 27 | }); 28 | const userIds = []; 29 | messageInfo.forEach((t) => !userIds.includes(t.userId) && userIds.push(t.userId)); 30 | const userInfos = await this.UserModel.find({ where: { id: In(userIds) }, select: ['id', 'nickname', 'avatar'] }); 31 | userInfos.forEach((t: any) => (t.userId = t.id)); 32 | messageInfo.forEach((t: any) => (t.userInfo = userInfos.find((k: any) => k.userId === t.userId))); 33 | return messageInfo; 34 | } 35 | 36 | async emoticon(params) { 37 | const { keyword } = params; 38 | const url = `https://www.doutula.com/search?keyword=${encodeURIComponent(keyword)}`; 39 | const $ = await requestHtml(url); 40 | const list = []; 41 | $('.search-result .pic-content .random_picture a').each((index, node) => { 42 | const url = $(node).find('img').attr('data-original'); 43 | url && list.push(url); 44 | }); 45 | return list; 46 | } 47 | 48 | async roomInfo(params) { 49 | const { roomId } = params; 50 | return await this.RoomModel.findOne({ 51 | where: { roomId }, 52 | select: ['roomId', 'roomName', 'roomNotice', 'roomNeedPassword', 'roomLogo', 'roomBg'], 53 | }); 54 | } 55 | 56 | async updateRoom(params, payload) { 57 | // TODO 刚刚创建token没有toomId 去tb_room拿 需要创建后交换新token 58 | const { userId } = payload; 59 | // const { roomId } = payload; 60 | // if (!roomId) { 61 | // throw new HttpException('非法操作、您还没开通个人房间!', HttpStatus.BAD_REQUEST); 62 | // } 63 | const room = await this.RoomModel.findOne({ where: { roomUserId: userId } }); 64 | const { roomId } = room; 65 | const { roomName, roomNotice, roomLogo } = params; 66 | const updateData: any = {}; 67 | roomName && (updateData.roomName = roomName); 68 | roomNotice && (updateData.roomNotice = roomNotice); 69 | roomLogo && (updateData.roomLogo = roomLogo); 70 | await this.RoomModel.update({ roomId }, updateData); 71 | return '修改房间信息成功!'; 72 | } 73 | 74 | async createRoom(params, payload) { 75 | const { roomId, userId, avatar } = payload; 76 | const { roomName, roomId: newRoomId, roomNotice } = params; 77 | if (roomId) { 78 | throw new HttpException('非法操作、您已经有个人房间了!', HttpStatus.BAD_REQUEST); 79 | } 80 | const oldRoom = await this.RoomModel.count({ where: { roomId: newRoomId } }); 81 | if (oldRoom) { 82 | throw new HttpException('当前房间号已经有人注册了、换个房间号吧!', HttpStatus.BAD_REQUEST); 83 | } 84 | await this.RoomModel.save({ 85 | roomName, 86 | roomId: newRoomId, 87 | roomNotice, 88 | roomUserId: userId, 89 | roomLogo: avatar, 90 | }); 91 | const res = await this.UserModel.update({ id: userId }, { roomId: newRoomId }); 92 | return res; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/chat/dto/room.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MinLength, MaxLength } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class createUpdateRoomInfoDto { 5 | // @ApiProperty({ example: '555', description: '房间ID', required: false }) 6 | // @MinLength(3, { message: '房间ID长度最短为3' }) 7 | // @MaxLength(3, { message: '房间ID长度最长为3' }) 8 | // roomId: number; 9 | 10 | @ApiProperty({ example: '555', description: '房间名称', required: false }) 11 | @MinLength(2, { message: '房间名称长度最短为2' }) 12 | @MaxLength(14, { message: '房间名称长度最长为14' }) 13 | roomName: string; 14 | 15 | @ApiProperty({ example: '555', description: '房间公告', required: false }) 16 | @MaxLength(300, { message: '房间公告长度最长为300' }) 17 | roomNotice: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/chat/dto/search.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class emoticonSearchDto { 5 | @ApiProperty({ example: '大笑', description: '关键词', required: true }) 6 | @IsNotEmpty({ message: '请输入您要搜索的表情包名称!' }) 7 | keyword: string; 8 | } 9 | 10 | export class roomInfoDto { 11 | @ApiProperty({ example: 666, description: '房间ID', required: true }) 12 | @IsNotEmpty({ message: '请填写房间ID!' }) 13 | roomId: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/chat/message.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from 'src/common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_message' }) 5 | export class MessageEntity extends BaseEntity { 6 | @Column() 7 | userId: number; 8 | 9 | @Column('text') 10 | content: string; 11 | 12 | @Column({ length: 64 }) 13 | type: string; 14 | 15 | @Column() 16 | roomId: number; 17 | 18 | @Column({ default: 1 }) 19 | status: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/chat/room.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from 'src/common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_room' }) 5 | export class RoomEntity extends BaseEntity { 6 | @Column({ unique: true }) 7 | roomUserId: number; 8 | 9 | @Column({ unique: true }) 10 | roomId: number; 11 | 12 | @Column({ length: 255, nullable: true }) 13 | roomLogo: string; 14 | 15 | @Column({ length: 255 }) 16 | roomName: string; 17 | 18 | @Column({ default: 0 }) 19 | roomNeedPassword: number; 20 | 21 | @Column({ length: 255, nullable: true }) 22 | roomPassword: string; 23 | 24 | @Column({ length: 255, default: '房间空空如也呢' }) 25 | roomNotice: string; 26 | 27 | @Column({ length: 255, nullable: true }) 28 | roomBg: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/collect/collect.controller.ts: -------------------------------------------------------------------------------- 1 | import { collectSetDto } from './dto/set.collect.dto'; 2 | import { CollectService } from './collect.service'; 3 | import { Controller, Post, Get, Body, Query, Request } from '@nestjs/common'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | 6 | @ApiTags('Collect') 7 | @Controller('collect') 8 | export class CollectController { 9 | constructor(private readonly collectService: CollectService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: collectSetDto, @Request() req) { 13 | return this.collectService.set(params, req); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/collect/collect.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from 'src/common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_collect' }) 5 | export class CollectEntity extends BaseEntity { 6 | @Column({ comment: '用户ID' }) 7 | userId: number; 8 | 9 | @Column({ default: 1, comment: '收藏类型,[1:音乐,2:文章,3:工具,4:导航,5:项目]' }) 10 | typeId: number; 11 | 12 | @Column({ nullable: true, comment: '歌曲ID' }) 13 | mid: number; 14 | 15 | @Column({ nullable: true, comment: '文章ID' }) 16 | articleId: number; 17 | 18 | @Column({ nullable: true, comment: '工具ID' }) 19 | toolId: number; 20 | 21 | @Column({ nullable: true, length: 255, comment: '收藏的歌曲封面图片' }) 22 | pic: string; 23 | 24 | @Column({ nullable: true, length: 255, comment: '收藏的歌曲专辑名称' }) 25 | artist: string; 26 | 27 | @Column({ nullable: true, length: 255, comment: '收藏的歌曲歌手名称' }) 28 | album: string; 29 | 30 | @Column({ nullable: true, length: 255, comment: '收藏的歌曲歌曲名称' }) 31 | name: string; 32 | 33 | @Column({ nullable: true, default: 0, comment: '是否未删除 1:删除' }) 34 | delete: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/collect/collect.module.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { CollectEntity } from './collect.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { CollectController } from './collect.controller'; 6 | import { CollectService } from './collect.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([CollectEntity, ArticleEntity])], 10 | controllers: [CollectController], 11 | providers: [CollectService], 12 | }) 13 | export class CollectModule {} 14 | -------------------------------------------------------------------------------- /src/modules/collect/collect.service.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { getNotEmptyKey } from './../../utils/tools'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { collectTypeMap, collectModuleMap } from './../../constant/collec'; 5 | import { Injectable, Module, HttpException, HttpStatus } from '@nestjs/common'; 6 | import { delEmptyCondition } from 'src/utils/tools'; 7 | import { CollectEntity } from './collect.entity'; 8 | import { Repository, LessThan } from 'typeorm'; 9 | 10 | @Injectable() 11 | export class CollectService { 12 | constructor( 13 | @InjectRepository(CollectEntity) 14 | private readonly CollectModule: Repository, 15 | @InjectRepository(ArticleEntity) 16 | private readonly ArticleModule: Repository, 17 | ) {} 18 | 19 | /** 20 | * @desc 收藏与取消收藏点赞 21 | * @param params 22 | * @returns 23 | */ 24 | async set(params, req) { 25 | const { articleId, toolId, mid, resourceId, typeId, projectId, isLike } = params; 26 | const { userId } = req.payload; 27 | const key = getNotEmptyKey({ articleId, toolId, mid, resourceId, projectId }); 28 | const data: any = {}; 29 | data.typeId = typeId; 30 | data.userId = userId; 31 | data[key] = params[key]; 32 | /* 点赞则添加新记录、取消点赞则伪删除保留记录 */ 33 | if (isLike === 1) { 34 | const isHasown = await this.CollectModule.findOne({ userId, [key]: params[key], delete: 0 }); 35 | if (isHasown) { 36 | throw new HttpException('已收藏过此项目!', HttpStatus.BAD_REQUEST); 37 | } 38 | await this.CollectModule.save(data); 39 | const module = collectModuleMap[key]; 40 | // TODO 查询的值和设置的值也需要根据typeId不同分类区分 暂时只做文章先不考虑 取消同理 或者其他表的收藏数字段设置为同名即可 41 | const result = await this[module].findOne({ where: { id: params[key] }, select: ['collectionVolume'] }); 42 | const collectionVolume = result ? result.collectionVolume + 1 : 1; 43 | await this[module].update({ id: params[key] }, { collectionVolume }); 44 | return true; 45 | } else { 46 | const module = collectModuleMap[key]; 47 | const result = await this[module].findOne({ where: { id: params[key] }, select: ['collectionVolume'] }); 48 | const collectionVolume = result ? result.collectionVolume - 1 : 0; 49 | await this[module].update({ id: params[key] }, { collectionVolume }); 50 | return await this.CollectModule.update({ [key]: params[key], userId }, { delete: 1 }); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/collect/dto/set.collect.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class collectSetDto { 6 | @ApiProperty({ example: '1', description: '收藏东西的类型' }) 7 | @IsNotEmpty({ message: '收藏类型不能为空!' }) 8 | typeId: number; 9 | 10 | @ApiProperty({ example: '1', description: '文章Id', required: false }) 11 | articleId: number; 12 | 13 | @ApiProperty({ example: '1', description: '工具Id', required: false }) 14 | toolId: number; 15 | 16 | @ApiProperty({ example: '1', description: '音乐Id', required: false }) 17 | mid: number; 18 | 19 | @ApiProperty({ example: '1', description: '资源导航Id', required: false }) 20 | resourceId: number; 21 | 22 | @ApiProperty({ example: '1', description: '项目Id', required: false }) 23 | projectId: number; 24 | 25 | @ApiProperty({ example: '1', description: '点赞或者取消点赞 1|0', required: false }) 26 | @IsOptional() 27 | @IsEnum([1, 0], { message: '状态参数非法' }) 28 | @Type(() => Number) 29 | isLike: number; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/comment/comment.controller.ts: -------------------------------------------------------------------------------- 1 | import { CommentSetDto } from './dto/comment.set.dto'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { CommentService } from './comment.service'; 4 | import { Controller, Post, Get, Body, Query, Request } from '@nestjs/common'; 5 | import { IpAddress } from 'src/decorator/ipAddress.guard'; 6 | 7 | @ApiTags('Comment') 8 | @Controller('/comment') 9 | export class CommentController { 10 | constructor(private readonly CommentService: CommentService) {} 11 | 12 | @Post('/set') 13 | set(@Body() params: CommentSetDto, @Request() req) { 14 | return this.CommentService.set(params, req); 15 | } 16 | 17 | @Get('/query') 18 | query(@Query() params) { 19 | return this.CommentService.query(params); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/comment/comment.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_comment' }) 5 | export class CommentEntity extends BaseEntity { 6 | @Column() 7 | userId: number; 8 | 9 | @Column() 10 | comment: string; 11 | 12 | @Column({ nullable: true }) 13 | articleId: number; 14 | 15 | @Column({ nullable: true }) 16 | upId: number; 17 | 18 | @Column({ nullable: true }) 19 | ip: string; 20 | 21 | @Column({ nullable: true }) 22 | address: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/comment/comment.module.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { CommentEntity } from './comment.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { CommentController } from './comment.controller'; 6 | import { CommentService } from './comment.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([CommentEntity, UserEntity])], 10 | controllers: [CommentController], 11 | providers: [CommentService], 12 | }) 13 | export class CommentModule {} 14 | -------------------------------------------------------------------------------- /src/modules/comment/comment.service.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { CommentEntity } from './comment.entity'; 4 | import { Repository, In, IsNull } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class CommentService { 9 | constructor( 10 | @InjectRepository(CommentEntity) 11 | private readonly CommentModel: Repository, 12 | @InjectRepository(UserEntity) 13 | private readonly UserModel: Repository, 14 | ) {} 15 | async set(params, req) { 16 | const { userId } = req.payload; 17 | const data = { ...params, userId }; 18 | return await this.CommentModel.save(data); 19 | } 20 | 21 | async query(params) { 22 | const { page = 1, pageSize = 10, status, articleId } = params; 23 | const where: any = { upId: null }; 24 | status && (where.status = status); 25 | articleId && (where.articleId = articleId); 26 | !articleId && (where.articleId = IsNull()); 27 | const rows = await this.CommentModel.find({ 28 | order: { id: 'DESC' }, 29 | where, 30 | skip: (page - 1) * pageSize, 31 | take: pageSize, 32 | cache: true, 33 | }); 34 | /* 查出当前评论及其所有的子评论 */ 35 | const commentIds = rows.map((t) => t.id); 36 | const childComment = await this.CommentModel.find({ upId: In([...new Set(commentIds)]) }); 37 | 38 | /* 拿到所有评论人的userId */ 39 | const upperIds = [...new Set(rows.map((t) => t.userId))]; 40 | const lowerIds = [...new Set(childComment.map((t) => t.userId))]; 41 | const userIds = [...new Set([...upperIds, ...lowerIds])]; 42 | const userInfo = await this.UserModel.find({ id: In(userIds) }); 43 | childComment.forEach((t: any) => { 44 | t.nickname = userInfo.find((k) => k.id === t.userId)['nickname']; 45 | t.avatar = userInfo.find((k) => k.id === t.userId)['avatar']; 46 | t.role = userInfo.find((k) => k.id === t.userId)['role']; 47 | }); 48 | rows.forEach((t: any) => { 49 | t.nickname = userInfo.find((k) => k.id === t.userId)['nickname']; 50 | t.avatar = userInfo.find((k) => k.id === t.userId)['avatar']; 51 | t.role = userInfo.find((k) => k.id === t.userId)['role']; 52 | t.chlidComment = childComment.filter((k) => t.id === k.upId); 53 | }); 54 | const count = await this.CommentModel.count(where); 55 | return { rows, count }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/comment/dto/comment.set.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class CommentSetDto { 5 | // @ApiProperty({ example: 1 , description: '用户id' }) 6 | // @IsNotEmpty({ message: '用户id不能为空' }) 7 | // userId: number; 8 | 9 | @ApiProperty({ example: '博主这篇文章写得很好啊,但是有这几点xxxx', description: '评论内容' }) 10 | @IsNotEmpty({ message: '评论内容不能为空' }) 11 | comment: string; 12 | 13 | @ApiProperty({ example: 1, description: '上一级的id,一级评论id', required: false }) 14 | upId: string; 15 | 16 | @ApiProperty({ example: 1, description: '文章id', required: false }) 17 | articleId: number; 18 | 19 | // @ApiProperty({ example: '188.35.62.4' , description: '评论的ip地址', required: false }) 20 | // ip: number; 21 | 22 | // @ApiProperty({ example: '陕西省 西安市' , description: '上传地址', required: false }) 23 | // address: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/common/common.service.ts: -------------------------------------------------------------------------------- 1 | import { TagEntity } from './../tag/tag.entity'; 2 | import { TypeEntity } from './../type/type.entity'; 3 | import { In, Repository } from 'typeorm'; 4 | import { UserEntity } from './../user/user.entity'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Injectable } from '@nestjs/common'; 7 | import MergeArticleInfo from './type'; 8 | @Injectable() 9 | export class CommonService { 10 | constructor( 11 | @InjectRepository(UserEntity) 12 | private readonly UserModel: Repository, 13 | @InjectRepository(TypeEntity) 14 | private readonly TypeModel: Repository, 15 | @InjectRepository(TagEntity) 16 | private readonly TagModel: Repository, 17 | ) {} 18 | 19 | /* 拿到所有用户名称映射回去 */ 20 | async findUserMap(ids) { 21 | return await this.UserModel.find({ where: { id: In(ids) }, select: ['id', 'username', 'nickname'] }).then((res) => { 22 | return res; 23 | }); 24 | } 25 | 26 | /* 拿到所有分类名称映射回去 */ 27 | async findTypeMap(ids) { 28 | return await this.TypeModel.find({ where: { id: In(ids) }, select: ['id', 'name'] }).then((res) => { 29 | return res; 30 | }); 31 | } 32 | 33 | /* 拿到所有标签映射回去 */ 34 | async findTagMap(ids) { 35 | return await this.TagModel.find({ where: { id: In(ids) }, select: ['id', 'name'] }).then((res) => { 36 | return res; 37 | }); 38 | } 39 | 40 | /* 合并数据映射回去,组装名称,标签,分类名 */ 41 | async mergeArticleInfo(mergeArticleInfo: MergeArticleInfo) { 42 | const { data, users, types, tags } = mergeArticleInfo; 43 | if (!data.length) return; 44 | data.forEach((item: any) => { 45 | users && (item.nickname = users.find((t: any) => t.id == item.userId)['nickname']); 46 | types && (item.typeName = types.find((t: any) => t.id == item.typeId)['name']); 47 | const tagIdArr = item.tagId.split(','); 48 | item.tagArr = []; 49 | tagIdArr.forEach((k) => item.tagArr.push(tags.find((t: any) => t.id == k))); 50 | delete item.content; 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modules/common/type.ts: -------------------------------------------------------------------------------- 1 | interface MergeArticleInfo { 2 | data: Array; 3 | users?: Array; 4 | types?: Array; 5 | tags?: Array; 6 | } 7 | 8 | export default MergeArticleInfo; 9 | -------------------------------------------------------------------------------- /src/modules/email/email.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { EmailService } from './email.service'; 4 | 5 | @ApiTags('Email') 6 | @Controller('email') 7 | export class EmailController { 8 | constructor(private readonly emailService: EmailService) {} 9 | 10 | @Get('/send') 11 | sendEmail(@Query() { text }) { 12 | return this.emailService.sendEmail(text); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/email/email.module.ts: -------------------------------------------------------------------------------- 1 | import { EmailService } from './email.service'; 2 | import { Module } from '@nestjs/common'; 3 | import { EmailController } from './email.controller'; 4 | 5 | @Module({ 6 | controllers: [EmailController], 7 | providers: [EmailService], 8 | }) 9 | export class EmailModule {} 10 | -------------------------------------------------------------------------------- /src/modules/email/email.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailerService } from '@nest-modules/mailer'; 3 | 4 | @Injectable() 5 | export class EmailService { 6 | constructor(private readonly mailerService: MailerService) {} 7 | 8 | async sendEmail(text: string) { 9 | const res = await this.mailerService.sendMail({ 10 | to: '927898639@qq.com', 11 | from: 'Nine_Team@163.com', 12 | subject: '来自Nine聊天室的注册验证', 13 | html: ` 14 | Nine Team 邮箱验证码 15 |

请点下以下链接激活您的账号,点此激活您的账号

16 | 来自 --小九的博客 17 |
`, 18 | template: './welcome', 19 | // context: { 20 | // // url: '', 21 | // }, 22 | }); 23 | return res; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/friend-links/dto/links.set.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsNotEmpty, IsOptional, IsEnum } from 'class-validator'; 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | 5 | export class LinksSetDto { 6 | @ApiProperty({ example: 1, description: '友链' }) 7 | @IsNotEmpty({ message: '友链不能为空' }) 8 | name: string; 9 | 10 | @ApiProperty({ example: 'http://xxxx.png', description: '添加友链头像logo', required: true }) 11 | avatar: string; 12 | 13 | @ApiProperty({ example: 'http://xxxx.com', description: '添加友链地址' }) 14 | @IsNotEmpty({ message: '友链地址不能为空' }) 15 | url: string; 16 | 17 | @ApiProperty({ example: '一个vue的博客', description: '友链描述简介' }) 18 | @IsNotEmpty({ message: '请填写友链描述简介' }) 19 | desc: string; 20 | 21 | @ApiProperty({ 22 | example: 1, 23 | description: '友链状态, 1:正常,-1:冻结', 24 | required: true, 25 | enum: [1, -1], 26 | name: 'status', 27 | }) 28 | @IsOptional() 29 | @IsEnum([1, -1], { message: '状态传入参数错误' }) 30 | @Type(() => Number) 31 | status: number; 32 | 33 | @ApiProperty({ example: 1, description: '自定义排序数字,数字越大越靠前', required: false }) 34 | orderId: number; 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/friend-links/friend-links.controller.ts: -------------------------------------------------------------------------------- 1 | import { LinksSetDto } from './dto/links.set.dto'; 2 | import { FriendLinksService } from './friend-links.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { Controller, Get, Post, Query, Body } from '@nestjs/common'; 5 | 6 | @ApiTags('FriendLinks') 7 | @Controller('friend-links') 8 | export class FriendLinksController { 9 | constructor(private readonly friendLinksService: FriendLinksService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: LinksSetDto) { 13 | return this.friendLinksService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.friendLinksService.query(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params) { 23 | return this.friendLinksService.del(params); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/friend-links/friend-links.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_friend_links' }) 5 | export class FriendLinksEntity extends BaseEntity { 6 | @Column() 7 | orderId: number; 8 | 9 | @Column({ length: 64 }) 10 | name: string; 11 | 12 | @Column() 13 | desc: string; 14 | 15 | @Column() 16 | avatar: string; 17 | 18 | @Column() 19 | url: string; 20 | 21 | @Column() 22 | status: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/friend-links/friend-links.module.ts: -------------------------------------------------------------------------------- 1 | import { FriendLinksEntity } from './friend-links.entity'; 2 | import { FriendLinksService } from './friend-links.service'; 3 | import { FriendLinksController } from './friend-links.controller'; 4 | import { TypeOrmModule } from '@nestjs/typeorm'; 5 | import { Module } from '@nestjs/common'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([FriendLinksEntity])], 9 | controllers: [FriendLinksController], 10 | providers: [FriendLinksService], 11 | }) 12 | export class FriendLinksModule {} 13 | -------------------------------------------------------------------------------- /src/modules/friend-links/friend-links.service.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm'; 2 | import { FriendLinksEntity } from './friend-links.entity'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Injectable } from '@nestjs/common'; 5 | 6 | @Injectable() 7 | export class FriendLinksService { 8 | constructor( 9 | @InjectRepository(FriendLinksEntity) 10 | private readonly FriendLinksModel: Repository, 11 | ) {} 12 | 13 | async set(params) { 14 | const { name, status, orderId, desc, url, avatar, id } = params; 15 | const data = { name, status, orderId, desc, url, avatar }; 16 | const f = await this.FriendLinksModel.findOne({ name }); 17 | if (f) { 18 | return await this.FriendLinksModel.update({ id: f.id }, data); 19 | } 20 | if (id) { 21 | return await this.FriendLinksModel.update({ id }, data); 22 | } 23 | const res = await this.FriendLinksModel.query(`select max(orderId) as max_order from tb_friend_links`); 24 | let { max_order } = res[0]; 25 | max_order = max_order > 0 ? max_order : 1; 26 | data.orderId = orderId ? orderId : max_order + 10; 27 | return await this.FriendLinksModel.save(data); 28 | } 29 | 30 | async query(params) { 31 | const { page = 1, pageSize = 10, status } = params; 32 | const where: any = {}; 33 | status && (where.status = status); 34 | const rows = await this.FriendLinksModel.find({ 35 | order: { orderId: 'DESC' }, 36 | where, 37 | skip: (page - 1) * pageSize, 38 | take: pageSize, 39 | cache: true, 40 | }); 41 | const count = await this.FriendLinksModel.count({ where }); 42 | return { rows, count }; 43 | } 44 | 45 | async del(params) { 46 | const { id } = params; 47 | return await this.FriendLinksModel.delete({ id }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/music/dto/music.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsInt, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class musicDto { 5 | @ApiProperty({ example: 175515991, description: '歌曲的mid', required: true }) 6 | @IsNotEmpty({ message: 'mid不能为空' }) 7 | mid: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/music/dto/search.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsInt, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class searchDto { 5 | @ApiProperty({ example: '孤城', description: '关键词', required: false }) 6 | keyword: string; 7 | 8 | @ApiProperty({ example: 1, description: '页码', required: false }) 9 | page: number; 10 | 11 | @ApiProperty({ example: 10, description: '单页数量', required: false }) 12 | pagesize: number; 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/music/music.controller.ts: -------------------------------------------------------------------------------- 1 | import { searchDto } from './dto/search.dto'; 2 | import { musicDto } from './dto/music.dto'; 3 | import { MusicService } from './music.service'; 4 | import { Controller, Injectable, Query, Get, Request } from '@nestjs/common'; 5 | import { ApiTags } from '@nestjs/swagger'; 6 | 7 | @ApiTags('Music') 8 | @Controller('music') 9 | export class MusicController { 10 | constructor(private readonly musicService: MusicService) {} 11 | 12 | @Get('/url') 13 | musicUrl(@Query() params: musicDto) { 14 | return this.musicService.musicUrl(params); 15 | } 16 | 17 | @Get('/info') 18 | musicInfo(@Query() params: musicDto) { 19 | return this.musicService.musicInfo(params); 20 | } 21 | 22 | @Get('/hot') 23 | hot(@Query() params) { 24 | return this.musicService.hot(params); 25 | } 26 | 27 | @Get('/list') 28 | musicHotList(@Query() params: musicDto) { 29 | return this.musicService.musicHotList(params); 30 | } 31 | 32 | @Get('/search') 33 | search(@Query() params: searchDto) { 34 | return this.musicService.search(params); 35 | } 36 | 37 | @Get('/mv') 38 | searchMv(@Query() params: musicDto) { 39 | return this.musicService.searchMv(params); 40 | } 41 | 42 | @Get('/collect') 43 | collect(@Request() req, @Query() params) { 44 | return this.musicService.collect(req.payload, params); 45 | } 46 | 47 | @Get('/remove') 48 | remove(@Request() req, @Query() params) { 49 | return this.musicService.remove(req.payload, params); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/music/music.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { Column, Entity, ManyToOne, OneToOne, JoinColumn } from 'typeorm'; 3 | import { BaseEntity } from 'src/common/entity/baseEntity'; 4 | 5 | @Entity({ name: 'tb_music' }) 6 | export class MusicEntity extends BaseEntity { 7 | @Column({ nullable: true }) 8 | customId: number; 9 | 10 | @Column({ length: 300 }) 11 | album: string; 12 | 13 | @Column() 14 | mid: number; 15 | 16 | @Column() 17 | duration: number; 18 | 19 | @Column({ length: 300 }) 20 | singer: string; 21 | 22 | @Column({ default: 0 }) 23 | hot: number; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/music/music.module.ts: -------------------------------------------------------------------------------- 1 | import { CollectEntity } from './../collect/collect.entity'; 2 | import { SpiderService } from './../spider/spider.service'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { MusicController } from './music.controller'; 6 | import { MusicService } from './music.service'; 7 | import { MusicEntity } from './music.entity'; 8 | 9 | @Module({ 10 | imports: [TypeOrmModule.forFeature([MusicEntity, CollectEntity])], 11 | controllers: [MusicController], 12 | providers: [MusicService, SpiderService], 13 | }) 14 | export class MusicModule {} 15 | -------------------------------------------------------------------------------- /src/modules/music/music.service.ts: -------------------------------------------------------------------------------- 1 | import { CollectEntity } from './../collect/collect.entity'; 2 | import { SpiderService } from './../spider/spider.service'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { requestInterface, spiderKuWoHotMusic } from './../../utils/spider'; 5 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 6 | import { MusicEntity } from './music.entity'; 7 | import { Repository } from 'typeorm'; 8 | 9 | @Injectable() 10 | export class MusicService { 11 | constructor( 12 | @InjectRepository(MusicEntity) 13 | private readonly MusicModel: Repository, 14 | @InjectRepository(CollectEntity) 15 | private readonly CollectModel: Repository, 16 | private readonly SpiderService: SpiderService, 17 | ) {} 18 | 19 | /* 通过mid查询歌曲地址 */ 20 | async musicUrl(params) { 21 | const { mid } = params; 22 | const url = `https://www.kuwo.cn/api/v1/www/music/playUrl?mid=${mid}&type=music&httpsStatus=1&reqId=853eeac0-3d6f-11ec-928a-dfe06ab55d81`; 23 | const res: any = await requestInterface(url); 24 | if (res.code === 200) { 25 | return res.data.url; 26 | } else { 27 | throw new HttpException(`没有找到歌曲地址!`, HttpStatus.BAD_REQUEST); 28 | } 29 | } 30 | 31 | /* 通过mid查询歌曲详细信息不包含歌词用不上了 */ 32 | async basemusicInfo(params) { 33 | const { mid } = params; 34 | const url = `https://www.kuwo.cn/api/www/music/musicInfo?mid=${mid}&httpsStatus=1&reqId=95974670-3eb3-11ec-8702-6df34f25d8f2`; 35 | const res: any = await requestInterface(url); 36 | if (res.code === 200) { 37 | const { artist, pic, pic120, duration, score100, album, songTimeMinutes, albuminfo, rid: mid } = res.data; 38 | return { artist, pic, pic120, duration, score100, album, songTimeMinutes, albuminfo, mid }; 39 | } else { 40 | throw new HttpException(`没有找到歌曲信息!`, HttpStatus.BAD_REQUEST); 41 | } 42 | } 43 | 44 | /* 查询热门歌曲 */ 45 | async hot(params) { 46 | const { page = 1, pagesize = 30 } = params; 47 | return await this.CollectModel.find({ 48 | where: { userId: 1, typeId: 1 }, 49 | order: { id: 'DESC' }, 50 | skip: (page - 1) * pagesize, 51 | take: pagesize, 52 | cache: true, 53 | }); 54 | } 55 | 56 | /* 查询歌曲详情包含歌词 */ 57 | async musicInfo(params) { 58 | const { mid } = params; 59 | 60 | const musicInfoUrl = `https://www.kuwo.cn/api/www/music/musicInfo?mid=${mid}&httpsStatus=1&reqId=0b8cd740-409f-11ec-af85-c164fd2658ed`; 61 | const lrcUrl = `https://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${mid}`; 62 | 63 | const musicInfoData: any = await requestInterface(musicInfoUrl); 64 | const lrcData: any = await requestInterface(lrcUrl); 65 | if (lrcData.status === 200 && musicInfoData.code === 200) { 66 | const { lrclist } = lrcData.data; 67 | const { artist, pic120: pic, duration, score100, album, songTimeMinutes, rid: mid } = musicInfoData.data; 68 | return { lrclist, musicInfo: { artist, pic, duration, score100, album, songTimeMinutes, mid } }; 69 | } else { 70 | throw new HttpException(`没有找到歌曲信息!`, HttpStatus.BAD_REQUEST); 71 | } 72 | } 73 | 74 | /* 查询搜索音乐 */ 75 | async search(params) { 76 | const { keyword, page = 1, pagesize = 10 } = params; 77 | let musicList: any; 78 | try { 79 | const res: any = await this.SpiderService.searchMusic({ keyword, page, pagesize }); 80 | if (res.code === 200) { 81 | musicList = res.data.list.map((t) => { 82 | const { rid: mid, duration, album, artist, albumpic, pic120, name, hasmv } = t; 83 | return { mid, duration, album, artist, albumpic, pic120, name, hasmv }; 84 | }); 85 | } 86 | } catch (error) { 87 | throw new HttpException(`没有搜索到歌曲`, HttpStatus.BAD_REQUEST); 88 | } 89 | return musicList; 90 | } 91 | 92 | /** 93 | * @desc 部分拥有mv的歌曲可以通过mid拿到视频地址 94 | */ 95 | async searchMv(params) { 96 | const { mid } = params; 97 | return await this.SpiderService.searchMv(mid); 98 | } 99 | 100 | async collect(payload, params) { 101 | const { page = 1, pagesize = 30 } = params; 102 | const { userId } = payload; 103 | return await this.CollectModel.find({ 104 | where: { userId, typeId: 1 }, 105 | order: { id: 'DESC' }, 106 | skip: (page - 1) * pagesize, 107 | take: pagesize, 108 | cache: true, 109 | }); 110 | } 111 | 112 | async remove(payload, params) { 113 | const { mid } = params; 114 | if (!payload) { 115 | throw new HttpException('请先登录', HttpStatus.UNAUTHORIZED); 116 | } 117 | const { userId } = payload; 118 | const u = await this.CollectModel.findOne({ where: { userId, mid } }); 119 | if (u) { 120 | await this.CollectModel.remove(u); 121 | return '移除完成'; 122 | } else { 123 | throw new HttpException('无权移除此歌曲!', HttpStatus.BAD_REQUEST); 124 | } 125 | } 126 | 127 | /** 128 | * @desc 每天十二点更新歌单,从这些分类随机选择一个分类拿到一个分类 然后获取前30首音乐,作为每天的歌单 129 | * 1.固定获取一个分类,拿到歌曲基本信息,通过mid可以拿到歌曲具体地址 130 | */ 131 | async musicHotList(params) { 132 | const { mid } = params; 133 | const res = await this.MusicModel.query(`select max(customId) as max from tb_music`); 134 | const lastMaxCustomId = res[0].max; 135 | const url = `https://www.kuwo.cn/playlist_detail/${mid}`; 136 | const musicList = await spiderKuWoHotMusic(url, lastMaxCustomId); 137 | return await this.MusicModel.save(musicList); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/project/dto/project.set.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsNotEmpty, IsOptional, IsEnum, ValidateNested } from 'class-validator'; 3 | import { ApiProperty } from '@nestjs/swagger'; 4 | 5 | class ProjectDto { 6 | @ApiProperty({ example: 1, description: '项目名称' }) 7 | @IsNotEmpty({ message: '项目名称不能为空' }) 8 | name: string; 9 | 10 | @ApiProperty({ example: 'http://xxxx.png', description: '项目卡片背景图' }) 11 | @IsNotEmpty({ message: '背景图图片不能为空' }) 12 | bgImage: string; 13 | 14 | @ApiProperty({ example: '一个关于xxx的项目。。。。', description: '项目介绍' }) 15 | @IsNotEmpty({ message: '请填写友链描述简介' }) 16 | desc: string; 17 | 18 | @ApiProperty({ example: '前端,vue', description: '项目标签 用,分割' }) 19 | @IsNotEmpty({ message: '项目标签不能为空' }) 20 | tag: string; 21 | 22 | @ApiProperty({ example: '2020-10-10', description: '项目开始时间' }) 23 | @IsNotEmpty({ message: '项目开始时间不能为空' }) 24 | startTime: string; 25 | 26 | @ApiProperty({ example: '2020-12-10', description: '项目结束时间' }) 27 | @IsNotEmpty({ message: '项目结束时间不能为空' }) 28 | endTime: string; 29 | 30 | @ApiProperty({ example: 'https://github/xxxx', description: '项目git地址', required: false }) 31 | git: string; 32 | 33 | @ApiProperty({ example: 'https://xxxx/xxxx', description: '项目demo预览地址', required: false }) 34 | link: string; 35 | 36 | @ApiProperty({ 37 | example: 1, 38 | description: '是否推荐项目, 1:是 -1:否', 39 | required: true, 40 | enum: [1, -1], 41 | name: 'status', 42 | }) 43 | @IsOptional() 44 | @IsEnum([1, -1], { message: 'hot传入参数错误' }) 45 | @Type(() => Number) 46 | hot: number; 47 | 48 | @ApiProperty({ 49 | example: 1, 50 | description: '项目类型, 1:外部地址,2:内部地址', 51 | required: true, 52 | enum: [1, 2], 53 | name: 'status', 54 | }) 55 | @IsOptional() 56 | @IsEnum([1, -1], { message: '项目类型传入参数错误' }) 57 | @Type(() => Number) 58 | type: number; 59 | 60 | @ApiProperty({ 61 | example: 1, 62 | description: '项目状态, 1:正常,-1:冻结', 63 | required: true, 64 | enum: [1, -1], 65 | name: 'status', 66 | }) 67 | @IsOptional() 68 | @IsEnum([1, -1], { message: '状态传入参数错误' }) 69 | @Type(() => Number) 70 | status: number; 71 | 72 | @ApiProperty({ example: 1, description: '自定义排序数字,数字越大越靠前', required: false }) 73 | orderId: number; 74 | } 75 | 76 | export class ProjectSetDto { 77 | @ApiProperty({ example: 1, description: '项目id' }) 78 | id: number; 79 | 80 | @ApiProperty({ example: 1, description: '项目名称' }) 81 | @ValidateNested() 82 | @Type(() => ProjectDto) 83 | data: ProjectDto; 84 | } 85 | -------------------------------------------------------------------------------- /src/modules/project/project.controller.ts: -------------------------------------------------------------------------------- 1 | import { ProjectSetDto } from './dto/project.set.dto'; 2 | import { ProjectService } from './project.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { Controller, Get, Post, Query, Body } from '@nestjs/common'; 5 | 6 | @ApiTags('Project') 7 | @Controller('project') 8 | export class ProjectController { 9 | constructor(private readonly ProjectService: ProjectService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: ProjectSetDto) { 13 | return this.ProjectService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.ProjectService.query(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params) { 23 | return this.ProjectService.del(params); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/project/project.module.ts: -------------------------------------------------------------------------------- 1 | import { ProjectController } from './project.controller'; 2 | import { ProjectEntity } from './projtct.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { ProjectService } from './project.service'; 6 | 7 | @Module({ 8 | imports: [TypeOrmModule.forFeature([ProjectEntity])], 9 | controllers: [ProjectController], 10 | providers: [ProjectService], 11 | }) 12 | export class ProjectModule {} 13 | -------------------------------------------------------------------------------- /src/modules/project/project.service.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm'; 2 | import { ProjectEntity } from './projtct.entity'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | 6 | @Injectable() 7 | export class ProjectService { 8 | constructor( 9 | @InjectRepository(ProjectEntity) 10 | private readonly ProjectModel: Repository, 11 | ) {} 12 | 13 | /* 添加编辑项目 */ 14 | async set(params) { 15 | const { data, id } = params; 16 | if (id) { 17 | return await this.ProjectModel.update({ id }, data); 18 | } 19 | const res = await this.ProjectModel.query(`select max(orderId) as max_order from tb_project`); 20 | let { max_order } = res[0]; 21 | max_order = max_order > 0 ? max_order : 1; 22 | data.orderId = data.orderId ? data.orderId : max_order + 10; 23 | return await this.ProjectModel.save(data); 24 | } 25 | 26 | async query(params) { 27 | const { page = 1, pageSize = 10, status } = params; 28 | const where: any = {}; 29 | status && (where.status = status); 30 | const rows = await this.ProjectModel.find({ 31 | order: { orderId: 'DESC' }, 32 | where, 33 | skip: (page - 1) * pageSize, 34 | take: pageSize, 35 | cache: true, 36 | }); 37 | const count = await this.ProjectModel.count({ where }); 38 | return { rows, count }; 39 | } 40 | 41 | async del(params) { 42 | const { id } = params; 43 | return await this.ProjectModel.delete({ id }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/project/projtct.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_project' }) 5 | export class ProjectEntity extends BaseEntity { 6 | @Column({ comment: '排序ID' }) 7 | orderId: number; 8 | 9 | @Column({ length: 64, comment: '项目名称' }) 10 | name: string; 11 | 12 | @Column({ comment: '项目介绍' }) 13 | desc: string; 14 | 15 | @Column({ comment: '背景图片地址' }) 16 | bgImage: string; 17 | 18 | @Column({ comment: '标签,用,切割存储' }) 19 | tag: string; 20 | 21 | @Column({ comment: '项目开始时间' }) 22 | startTime: Date; 23 | 24 | @Column({ comment: '项目结束时间' }) 25 | endTime: Date; 26 | 27 | @Column({ comment: '项目git地址', nullable: true }) 28 | git: string; 29 | 30 | @Column({ comment: '项目demo示例地址', nullable: true }) 31 | link: string; 32 | 33 | @Column({ comment: '类型:[1:项目,可以跳转到外部 2:博客内部模块,为2就走path本站跳转]', nullable: true }) 34 | type: number; 35 | 36 | @Column({ comment: '本站路径, type == 2 才会有path', nullable: true }) 37 | path: string; 38 | 39 | @Column({ comment: '是否是热门项目 推荐项目 [1: 推荐 -1:默认]', nullable: true }) 40 | hot: number; 41 | 42 | @Column({ comment: '项目状态 [1: 正常 -1:冻结]', nullable: true }) 43 | status: number; 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/resource-type/dto/resourceType.set.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class ResourceTypeSetDto { 5 | @ApiProperty({ example: 1, description: '添加资源分类名称', name: 'name' }) 6 | @IsNotEmpty({ message: '分类名称不能为空' }) 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/resource-type/resource-type.controller.ts: -------------------------------------------------------------------------------- 1 | import { ResourceTypeSetDto } from './dto/resourceType.set.dto'; 2 | import { ResourceTypeService } from './resource-type.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { Controller, Post, Body, Query, Get } from '@nestjs/common'; 5 | 6 | @ApiTags('ResourceType') 7 | @Controller('/resourceType') 8 | export class ResourceTypeController { 9 | constructor(private readonly resourceTypeService: ResourceTypeService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: ResourceTypeSetDto) { 13 | return this.resourceTypeService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.resourceTypeService.query(params); 19 | } 20 | 21 | @Get('/queryAll') 22 | queryAll(@Query() params) { 23 | return this.resourceTypeService.queryAll(params); 24 | } 25 | 26 | @Post('/del') 27 | del(@Body() params) { 28 | return this.resourceTypeService.del(params); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/resource-type/resource-type.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_resource_type' }) 5 | export class ResourceTypeEntity extends BaseEntity { 6 | @Column({ unique: true }) 7 | orderId: number; 8 | 9 | @Column({ length: 16 }) 10 | name: string; 11 | 12 | @Column({ default: 1 }) 13 | status: number; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/resource-type/resource-type.module.ts: -------------------------------------------------------------------------------- 1 | import { ResourceEntity } from '../resource/resource.entity'; 2 | import { ResourceTypeEntity } from './resource-type.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { ResourceTypeController } from './resource-type.controller'; 6 | import { ResourceTypeService } from './resource-type.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([ResourceTypeEntity, ResourceEntity])], 10 | controllers: [ResourceTypeController], 11 | providers: [ResourceTypeService], 12 | }) 13 | export class ResourceTypeModule {} 14 | -------------------------------------------------------------------------------- /src/modules/resource-type/resource-type.service.ts: -------------------------------------------------------------------------------- 1 | import { ResourceEntity } from '../resource/resource.entity'; 2 | import { Repository, In } from 'typeorm'; 3 | import { ResourceTypeEntity } from './resource-type.entity'; 4 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class ResourceTypeService { 9 | constructor( 10 | @InjectRepository(ResourceTypeEntity) 11 | private readonly ResourceTypeModel: Repository, 12 | @InjectRepository(ResourceEntity) 13 | private readonly ResourceModel: Repository, 14 | ) {} 15 | 16 | async set(params) { 17 | const { name, status, orderId, id } = params; 18 | const data: any = { name, orderId, status }; 19 | const r = await this.ResourceTypeModel.findOne({ name }); 20 | if (r) { 21 | return await this.ResourceTypeModel.update({ id: r.id }, data); 22 | } 23 | if (id) { 24 | return await this.ResourceTypeModel.update({ id }, data); 25 | } 26 | const res = await this.ResourceTypeModel.query(`select max(orderId) as max_order from tb_resource_type`); 27 | let { max_order } = res[0]; 28 | max_order = max_order > 0 ? max_order : 1; 29 | data.orderId = orderId ? orderId : max_order + 10; 30 | return await this.ResourceTypeModel.save(data); 31 | } 32 | 33 | async query(params) { 34 | const { page = 1, pageSize = 10 } = params; 35 | const rows = await this.ResourceTypeModel.find({ 36 | order: { id: 'ASC' }, 37 | skip: (page - 1) * pageSize, 38 | take: pageSize, 39 | cache: true, 40 | }); 41 | const ids = rows.map((t) => t.id); 42 | const res: any = await this.ResourceModel.find({ resourceId: In(ids) }); 43 | rows.forEach((t: any) => (t.resource_num = res.filter((k) => k.resourceId === t.id).length)); 44 | const count = await this.ResourceTypeModel.count(); 45 | return { rows, count }; 46 | } 47 | 48 | async queryAll(params) { 49 | // const {page = 1,pageSize = 10} = params 50 | const rows = await this.ResourceTypeModel.find({ 51 | order: { orderId: 'ASC' }, 52 | where: { status: 1 }, 53 | // skip: (page - 1) * pageSize, 54 | // take: pageSize, 55 | cache: true, 56 | }); 57 | const resourceIds = rows.map((t) => t.id); 58 | const res: any = await this.ResourceModel.find({ resourceId: In(resourceIds) }); 59 | rows.forEach((t: any) => (t.resource = res.filter((k) => k.resourceId == t.id))); 60 | const count = await this.ResourceTypeModel.count(); 61 | return { rows, count }; 62 | } 63 | 64 | async del(params) { 65 | const { id } = params; 66 | const count = await this.ResourceModel.count({ resourceId: id }); 67 | if (count > 0) { 68 | throw new HttpException('当前分类正在使用中!', HttpStatus.BAD_REQUEST); 69 | } 70 | const r = await this.ResourceTypeModel.findOne({ id }); 71 | if (!r) { 72 | throw new HttpException('非法操作,无此分类!', HttpStatus.BAD_REQUEST); 73 | } 74 | return await this.ResourceTypeModel.delete({ id }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/modules/resource/dto/resource.set.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class ResourceSetDto { 5 | @ApiProperty({ example: 1, description: '资源分类', name: 'resourceId' }) 6 | @IsNotEmpty({ message: '资源分类不能为空' }) 7 | resourceId: number; 8 | 9 | @ApiProperty({ example: 'http://xxxx.png', description: '资源LOGO', name: 'logo', required: false }) 10 | logo: string; 11 | 12 | @ApiProperty({ example: 'vue', description: '添加资源名称', name: 'name' }) 13 | @IsNotEmpty({ message: '资源名称不能为空' }) 14 | name: string; 15 | 16 | @ApiProperty({ example: '这是一个前端框架', description: '添加资源描述', name: 'desc' }) 17 | @IsNotEmpty({ message: '资源描述不能为空' }) 18 | desc: string; 19 | 20 | @ApiProperty({ example: 'www.baidu.com', description: '添加资源指向地址', name: 'url' }) 21 | @IsNotEmpty({ message: '资源指向地址不能为空' }) 22 | url: string; 23 | 24 | @ApiProperty({ example: 1, description: '自定义排序数字', name: 'orderId', required: false }) 25 | orderId: number; 26 | } 27 | -------------------------------------------------------------------------------- /src/modules/resource/resource.controller.ts: -------------------------------------------------------------------------------- 1 | import { ResourceSetDto } from './dto/resource.set.dto'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { ResourceService } from './resource.service'; 4 | import { Controller, Post, Get, Body, Query } from '@nestjs/common'; 5 | 6 | @ApiTags('Resource') 7 | @Controller('/resource') 8 | export class ResourceController { 9 | constructor(private readonly resourceService: ResourceService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: ResourceSetDto) { 13 | return this.resourceService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.resourceService.query(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params) { 23 | return this.resourceService.del(params); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/resource/resource.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_resource' }) 5 | export class ResourceEntity extends BaseEntity { 6 | @Column({ nullable: true, comment: '排序id' }) 7 | orderId: number; 8 | 9 | @Column({ length: 32, comment: '资源名称' }) 10 | name: string; 11 | 12 | @Column({ comment: '资源描述' }) 13 | desc: string; 14 | 15 | @Column({ nullable: true, comment: '资源logo' }) 16 | logo: string; 17 | 18 | @Column({ comment: '资源地址' }) 19 | url: string; 20 | 21 | @Column({ comment: '资源分类ID' }) 22 | resourceId: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/resource/resource.module.ts: -------------------------------------------------------------------------------- 1 | import { ResourceTypeEntity } from '../resource-type/resource-type.entity'; 2 | import { ResourceEntity } from './resource.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { ResourceController } from './resource.controller'; 6 | import { ResourceService } from './resource.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([ResourceEntity, ResourceTypeEntity])], 10 | controllers: [ResourceController], 11 | providers: [ResourceService], 12 | }) 13 | export class ResourceModule {} 14 | -------------------------------------------------------------------------------- /src/modules/resource/resource.service.ts: -------------------------------------------------------------------------------- 1 | import { Like } from 'typeorm'; 2 | import { ResourceTypeEntity } from '../resource-type/resource-type.entity'; 3 | import { ResourceEntity } from './resource.entity'; 4 | import { Repository, In } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | import { Injectable } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class ResourceService { 10 | constructor( 11 | @InjectRepository(ResourceEntity) 12 | private readonly ResourceModel: Repository, 13 | @InjectRepository(ResourceTypeEntity) 14 | private readonly ResourceTypeModel: Repository, 15 | ) {} 16 | 17 | async set(params) { 18 | const { resourceId, logo, name, desc, url, orderId, id } = params; 19 | const data = { resourceId, logo, name, desc, url, orderId }; 20 | const r = await this.ResourceModel.findOne({ name }); 21 | if (r) { 22 | return await this.ResourceModel.update({ id: r.id }, data); 23 | } 24 | if (id) { 25 | return await this.ResourceModel.update({ id }, data); 26 | } 27 | const res = await this.ResourceModel.query(`select max(orderId) as max_order from tb_resource`); 28 | let { max_order } = res[0]; 29 | max_order = max_order > 0 ? max_order : 1; 30 | data.orderId = orderId ? orderId : max_order + 10; 31 | return await this.ResourceModel.save(data); 32 | } 33 | 34 | async query(params) { 35 | const { page = 1, pageSize = 10, resourceId, name } = params; 36 | const where: any = {}; 37 | resourceId && (where.resourceId = resourceId); 38 | name && (where.name = Like(`%${name}%`)); 39 | const rows = await this.ResourceModel.find({ 40 | order: { id: 'DESC' }, 41 | where, 42 | skip: (page - 1) * pageSize, 43 | take: pageSize, 44 | cache: true, 45 | }); 46 | const resourceIds = rows.map((t) => t.resourceId); 47 | const recourceType = await this.ResourceTypeModel.find({ id: In(resourceIds) }); 48 | rows.forEach((t: any) => (t.resourceName = recourceType.find((k) => k.id == t.resourceId)['name'])); 49 | const count = await this.ResourceModel.count({ where }); 50 | return { rows, count }; 51 | } 52 | 53 | async del(params) { 54 | const { id } = params; 55 | return await this.ResourceModel.delete({ id }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/modules/spider/dto/url.music.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsInt, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class urlMusicDto { 5 | @ApiProperty({ example: 175515991, description: '歌曲的mid', required: true }) 6 | @IsNotEmpty({ message: 'mid不能为空' }) 7 | mid: number; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/spider/spider.controller.ts: -------------------------------------------------------------------------------- 1 | import { urlMusicDto } from './dto/url.music.dto'; 2 | import { SpiderService } from './spider.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { Controller, Get, Body, Query } from '@nestjs/common'; 5 | 6 | // const html2md = require('html-to-md'); 7 | @ApiTags('Spider') 8 | @Controller('/spider') 9 | export class SpiderController { 10 | constructor(private readonly spiderService: SpiderService) {} 11 | 12 | @Get('/test') 13 | test(@Query() params: urlMusicDto) { 14 | return this.spiderService.test(params); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/spider/spider.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SpiderService } from './spider.service'; 3 | import { SpiderController } from './spider.controller'; 4 | 5 | @Module({ 6 | providers: [SpiderService], 7 | controllers: [SpiderController], 8 | }) 9 | export class SpiderModule {} 10 | -------------------------------------------------------------------------------- /src/modules/spider/spider.service.ts: -------------------------------------------------------------------------------- 1 | import { requestInterface, searchMusic, requestHtml } from './../../utils/spider'; 2 | import { Injectable } from '@nestjs/common'; 3 | import * as https from 'https'; 4 | import * as cheerio from 'cheerio'; 5 | // import * as html2md from 'html-to-md' 6 | const html2md = require('html-to-md'); 7 | 8 | @Injectable() 9 | export class SpiderService { 10 | constructor() {} 11 | 12 | async test(params) { 13 | const { mid } = params; 14 | const url = `https://www.kuwo.cn/api/www/music/musicInfo?mid=${mid}&httpsStatus=1&reqId=0b8cd740-409f-11ec-af85-c164fd2658ed`; 15 | const res = await requestInterface(url); 16 | 17 | return res; 18 | } 19 | 20 | async searchMusic({ keyword, page = 1, pagesize = 30 }) { 21 | keyword = encodeURIComponent(keyword); 22 | const url = `https://www.kuwo.cn/api/www/search/searchMusicBykeyWord?key=${keyword}&pn=${page}&rn=${pagesize}&httpsStatus=1&reqId=443229f0-3f29-11ec-a345-4125bd2a21d6`; 23 | return await searchMusic(url); 24 | } 25 | 26 | async searchMv(mid) { 27 | const url = `https://www.kuwo.cn/api/v1/www/music/playUrl?mid=${mid}&type=mv&httpsStatus=1&reqId=f8064a10-3f2e-11ec-8157-6fda69b0bb2a`; 28 | return await requestInterface(url); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/statistics/statistics.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiTags } from '@nestjs/swagger'; 2 | import { StatisticsService } from './statistics.service'; 3 | import { Controller, Post, Get, Body, Query } from '@nestjs/common'; 4 | 5 | @ApiTags('Statisics') 6 | @Controller('statistics') 7 | export class StatisticsController { 8 | constructor(private readonly statisticsService: StatisticsService) {} 9 | @Get('/visit') 10 | query(@Query() params) { 11 | return this.statisticsService.visit(params); 12 | } 13 | 14 | @Get('/typeInfo') 15 | typeInfo() { 16 | return this.statisticsService.typeInfo(); 17 | } 18 | 19 | @Get('/summary') 20 | summary() { 21 | return this.statisticsService.summary(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/statistics/statistics.module.ts: -------------------------------------------------------------------------------- 1 | import { FriendLinksEntity } from './../friend-links/friend-links.entity'; 2 | import { TypeEntity } from './../type/type.entity'; 3 | import { CommentEntity } from './../comment/comment.entity'; 4 | import { ArticleEntity } from './../article/article.entity'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { Module } from '@nestjs/common'; 7 | import { StatisticsController } from './statistics.controller'; 8 | import { StatisticsService } from './statistics.service'; 9 | import { MessageEntity } from '../chat/message.entity'; 10 | import { UserEntity } from '../user/user.entity'; 11 | 12 | @Module({ 13 | imports: [ 14 | TypeOrmModule.forFeature([ArticleEntity, CommentEntity, MessageEntity, TypeEntity, FriendLinksEntity, UserEntity]), 15 | ], 16 | controllers: [StatisticsController], 17 | providers: [StatisticsService], 18 | }) 19 | export class StatisticsModule {} 20 | -------------------------------------------------------------------------------- /src/modules/statistics/statistics.service.ts: -------------------------------------------------------------------------------- 1 | import { FriendLinksEntity } from './../friend-links/friend-links.entity'; 2 | import { CommentEntity } from './../comment/comment.entity'; 3 | import { formatBaiduReq } from './../../utils/date'; 4 | import { ArticleEntity } from './../article/article.entity'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { Repository, IsNull } from 'typeorm'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import axios from 'axios'; 9 | import { TypeEntity } from '../type/type.entity'; 10 | import { UserEntity } from '../user/user.entity'; 11 | 12 | @Injectable() 13 | export class StatisticsService { 14 | constructor( 15 | @InjectRepository(ArticleEntity) 16 | private readonly ArticleModel: Repository, 17 | @InjectRepository(TypeEntity) 18 | private readonly TypeModel: Repository, 19 | @InjectRepository(CommentEntity) 20 | private readonly CommentModel: Repository, 21 | @InjectRepository(FriendLinksEntity) 22 | private readonly FriendLinksModel: Repository, 23 | @InjectRepository(UserEntity) 24 | private readonly UserModel: Repository, 25 | ) {} 26 | 27 | /** 28 | * @desc 查询网站访问量 pv uv ip 29 | * token刷新一个月有效期 https://tongji.baidu.com/api/manual/Chapter2/openapi.html 30 | * TODO 待添加自动更新token 31 | * @param params 32 | * @returns 33 | */ 34 | async visit(params) { 35 | const { date } = params; 36 | const start_date = formatBaiduReq(new Date().getTime()); 37 | const end_date = formatBaiduReq(new Date().getTime() - Number(date - 1) * 24 * 60 * 60 * 1000); 38 | const url = `https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=121.f313de9dd8b1ed7bc4bfc248a3055052.YlwA80yYxHjxMa31pBtfmwJh5kDgfsk7sTKBGzw.EASTlg&site_id=17558179&method=overview/getTimeTrendRpt&start_date=${end_date}&end_date=${start_date}&metrics=pv_count,visitor_count,ip_count`; 39 | const res = await axios.get(url); 40 | return res.data; 41 | } 42 | 43 | /** 44 | * @desc 获取各个分类的文章数量 45 | * @returns 46 | */ 47 | async typeInfo() { 48 | const type = await this.TypeModel.find({ select: ['id', 'name'] }); 49 | const typeIds = type.map((t: any) => t.id); 50 | const task = []; 51 | typeIds.forEach((id) => task.push(this.ArticleModel.count({ where: { typeId: id } }))); 52 | const articleNumsMap = await Promise.all(task); 53 | type.forEach((t: any, i) => (t.nums = articleNumsMap[i])); 54 | return type; 55 | } 56 | 57 | /** 58 | * @desc 获取汇总信息 59 | */ 60 | async summary() { 61 | const articleNum = await this.ArticleModel.count(); 62 | const draftNum = await this.ArticleModel.count({ where: { status: -1 } }); 63 | const leaveMsgNum = await this.CommentModel.count({ where: { articleId: IsNull() } }); 64 | const friendLinkNum = await this.FriendLinksModel.count({ where: { status: 1 } }); 65 | const userNum = await this.UserModel.count(); 66 | return { articleNum, draftNum, leaveMsgNum, friendLinkNum, userNum }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/tag/dto/set.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MaxLength } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class TagSetDto { 5 | @ApiProperty({ example: 'Vue', description: '标签名称' }) 6 | @IsNotEmpty({ message: '标签名称不能为空' }) 7 | @MaxLength(9, { message: '标签最长不能超过9位' }) 8 | name: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/tag/tag.controller.ts: -------------------------------------------------------------------------------- 1 | import { TagSetDto } from './dto/set.dto'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { TagService } from './tag.service'; 4 | import { Controller, Post, Body, Get, Query } from '@nestjs/common'; 5 | 6 | @ApiTags('Tag') 7 | @Controller('tag') 8 | export class TagController { 9 | constructor(private readonly tagService: TagService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: TagSetDto) { 13 | return this.tagService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.tagService.query(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params) { 23 | return this.tagService.del(params); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/tag/tag.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_tag' }) 5 | export class TagEntity extends BaseEntity { 6 | @Column({ length: 16, unique: true }) 7 | name: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/tag/tag.module.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { TagEntity } from './tag.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { TagController } from './tag.controller'; 6 | import { TagService } from './tag.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([TagEntity, ArticleEntity])], 10 | controllers: [TagController], 11 | providers: [TagService], 12 | }) 13 | export class TagModule {} 14 | -------------------------------------------------------------------------------- /src/modules/tag/tag.service.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { Repository } from 'typeorm'; 3 | import { TagEntity } from './tag.entity'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 6 | 7 | @Injectable() 8 | export class TagService { 9 | constructor( 10 | @InjectRepository(TagEntity) 11 | private readonly TagModel: Repository, 12 | @InjectRepository(ArticleEntity) 13 | private readonly ArticleModel: Repository, 14 | ) {} 15 | 16 | async set(params) { 17 | const { name, id } = params; 18 | const tag = await this.TagModel.findOne({ name }); 19 | if (tag) { 20 | return await this.TagModel.update({ id: tag.id }, { name }); 21 | } 22 | if (id) { 23 | return await this.TagModel.update({ id }, { name }); 24 | } 25 | return await this.TagModel.save({ name }); 26 | } 27 | 28 | async query(params) { 29 | const { page = 1, pageSize = 10 } = params; 30 | const rows = await this.TagModel.find({ 31 | order: { id: 'DESC' }, 32 | skip: (page - 1) * pageSize, 33 | take: pageSize, 34 | cache: false, 35 | }); 36 | const count = await this.TagModel.count(); 37 | return { rows, count }; 38 | } 39 | 40 | async del(params) { 41 | const { id } = params; 42 | const count = await this.ArticleModel.count({ tagId: id }); 43 | if (count > 0) { 44 | throw new HttpException('该标签有文章正在使用中!', HttpStatus.BAD_REQUEST); 45 | } 46 | return await this.TagModel.delete({ id }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/modules/tools-type/tools-type.controller.ts: -------------------------------------------------------------------------------- 1 | import { ToolsTypeService } from './tools-type.service'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Body, Controller, Get, Post, Query } from '@nestjs/common'; 4 | 5 | @ApiTags('ToolsType') 6 | @Controller('/toolsType') 7 | export class ToolsTypeController { 8 | constructor(private readonly toolsTypeService: ToolsTypeService) {} 9 | 10 | @Get('/query') 11 | query(@Query() params) { 12 | return this.toolsTypeService.query(params); 13 | } 14 | 15 | @Post('/set') 16 | set(@Body() params) { 17 | return this.toolsTypeService.set(params); 18 | } 19 | 20 | @Post('/del') 21 | del(@Body() params) { 22 | return this.toolsTypeService.del(params); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/tools-type/tools-type.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_tools_type' }) 5 | export class ToolsTypeEntity extends BaseEntity { 6 | @Column({ unique: true }) 7 | orderId: number; 8 | 9 | @Column({ length: 32 }) 10 | name: string; 11 | 12 | @Column({ length: 255, default: '分类描述' }) 13 | desc: string; 14 | 15 | @Column({ default: 1 }) 16 | status: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/tools-type/tools-type.module.ts: -------------------------------------------------------------------------------- 1 | import { ToolsEntity } from './../tools/tools.entity'; 2 | import { ToolsTypeEntity } from './tools-type.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { ToolsTypeController } from './tools-type.controller'; 6 | import { ToolsTypeService } from './tools-type.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([ToolsTypeEntity, ToolsEntity])], 10 | controllers: [ToolsTypeController], 11 | providers: [ToolsTypeService], 12 | }) 13 | export class ToolsTypeModule {} 14 | -------------------------------------------------------------------------------- /src/modules/tools-type/tools-type.service.ts: -------------------------------------------------------------------------------- 1 | import { ToolsEntity } from './../tools/tools.entity'; 2 | import { Repository, In } from 'typeorm'; 3 | import { ToolsTypeEntity } from './tools-type.entity'; 4 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class ToolsTypeService { 9 | constructor( 10 | @InjectRepository(ToolsTypeEntity) 11 | private readonly toolsTypeModel: Repository, 12 | @InjectRepository(ToolsEntity) 13 | private readonly toolsModel: Repository, 14 | ) {} 15 | 16 | /** 17 | * @desc 工具分类新增编辑 18 | * @param params 19 | * @returns 20 | */ 21 | async set(params) { 22 | const { name, status, orderId, id } = params; 23 | const data: any = { name, orderId, status }; 24 | const r = await this.toolsTypeModel.findOne({ name }); 25 | if (r) { 26 | return await this.toolsTypeModel.update({ id: r.id }, data); 27 | } 28 | if (id) { 29 | return await this.toolsTypeModel.update({ id }, data); 30 | } 31 | const res = await this.toolsTypeModel.query(`select max(orderId) as max_order from tb_tools_type`); 32 | let { max_order } = res[0]; 33 | max_order = max_order > 0 ? max_order : 1; 34 | data.orderId = orderId ? orderId : max_order + 10; 35 | return await this.toolsTypeModel.save(data); 36 | } 37 | 38 | /** 39 | * @desc 工具分类查询 40 | * @param params 41 | * @returns 42 | */ 43 | async query(params) { 44 | const { page = 1, pageSize = 10 } = params; 45 | const rows = await this.toolsTypeModel.find({ 46 | order: { orderId: 'DESC' }, 47 | skip: (page - 1) * pageSize, 48 | take: pageSize, 49 | cache: true, 50 | }); 51 | const ids = rows.map((t) => t.id); 52 | const res: any = await this.toolsModel.find({ typeId: In(ids) }); 53 | rows.forEach((t: any) => (t.tools_num = res.filter((k) => k.typeId === t.id).length)); 54 | const count = await this.toolsTypeModel.count(); 55 | return { rows, count }; 56 | } 57 | 58 | async del(params) { 59 | const { id } = params; 60 | const count = await this.toolsModel.count({ typeId: id }); 61 | if (count > 0) { 62 | throw new HttpException('当前分类正在使用中!', HttpStatus.BAD_REQUEST); 63 | } 64 | const r = await this.toolsTypeModel.findOne({ id }); 65 | if (!r) { 66 | throw new HttpException('非法操作,无此分类!', HttpStatus.BAD_REQUEST); 67 | } 68 | return await this.toolsTypeModel.delete({ id }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/tools/dto/tools.douyin.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class toolsDouyinDto { 5 | @ApiProperty({ 6 | example: 'https://v.douyin.com/LFxv9ot/', 7 | description: '视频地址、点击右下角分享点击复制连接', 8 | required: true, 9 | }) 10 | @IsNotEmpty({ message: '视频地址不能为空' }) 11 | url: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/tools/tools.controller.ts: -------------------------------------------------------------------------------- 1 | import { toolsDouyinDto } from './dto/tools.douyin.dto'; 2 | import { ToolsService } from './tools.service'; 3 | import { ApiTags } from '@nestjs/swagger'; 4 | import { Body, Controller, Get, Post, Query, Response } from '@nestjs/common'; 5 | 6 | @ApiTags('Tools') 7 | @Controller('tools') 8 | export class ToolsController { 9 | constructor(private readonly toolsService: ToolsService) {} 10 | 11 | @Get('/query') 12 | query(@Query() params) { 13 | return this.toolsService.query(params); 14 | } 15 | 16 | @Post('/set') 17 | set(@Body() params) { 18 | return this.toolsService.set(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params) { 23 | return this.toolsService.del(params); 24 | } 25 | 26 | @Get('/douyin') 27 | douyin(@Query() params: toolsDouyinDto) { 28 | return this.toolsService.douyin(params); 29 | } 30 | 31 | @Get('/douyinload') 32 | douyinload(@Query() params: toolsDouyinDto, @Response() res) { 33 | return this.toolsService.douyinload(params, res); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/modules/tools/tools.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity } from 'typeorm'; 2 | import { BaseEntity } from '../../common/entity/baseEntity'; 3 | 4 | @Entity({ name: 'tb_tools' }) 5 | export class ToolsEntity extends BaseEntity { 6 | @Column({ unique: true }) 7 | orderId: number; 8 | 9 | @Column({ length: 32 }) 10 | name: string; 11 | 12 | @Column() 13 | typeId: number; 14 | 15 | @Column({ length: 500 }) 16 | logo: string; 17 | 18 | @Column({ length: 255, nullable: true }) 19 | desc: string; 20 | 21 | @Column({ length: 255, nullable: true }) 22 | path: string; 23 | 24 | @Column({ length: 255, nullable: true }) 25 | url: string; 26 | 27 | @Column({ default: 1 }) 28 | status: number; 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/tools/tools.module.ts: -------------------------------------------------------------------------------- 1 | import { ToolsTypeEntity } from './../tools-type/tools-type.entity'; 2 | import { ToolsEntity } from './tools.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { ToolsController } from './tools.controller'; 6 | import { ToolsService } from './tools.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([ToolsEntity,ToolsTypeEntity])], 10 | controllers: [ToolsController], 11 | providers: [ToolsService], 12 | }) 13 | export class ToolsModule {} 14 | -------------------------------------------------------------------------------- /src/modules/tools/tools.service.ts: -------------------------------------------------------------------------------- 1 | import { ToolsTypeEntity } from './../tools-type/tools-type.entity'; 2 | import { ToolsEntity } from './tools.entity'; 3 | import { Injectable } from '@nestjs/common'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import axios from 'axios'; 6 | import { Repository, In, Like } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class ToolsService { 10 | constructor( 11 | @InjectRepository(ToolsEntity) 12 | private readonly toolsModel: Repository, 13 | @InjectRepository(ToolsTypeEntity) 14 | private readonly toolsTypeModel: Repository, 15 | ) {} 16 | 17 | async set(params) { 18 | const { typeId, logo, name, desc, url, orderId, id, path } = params; 19 | const data = { typeId, logo, name, desc, url, orderId, path }; 20 | const r = await this.toolsModel.findOne({ name }); 21 | if (r) { 22 | return await this.toolsModel.update({ id: r.id }, data); 23 | } 24 | if (id) { 25 | return await this.toolsModel.update({ id }, data); 26 | } 27 | const res = await this.toolsModel.query(`select max(orderId) as max_order from tb_tools`); 28 | let { max_order } = res[0]; 29 | max_order = max_order > 0 ? max_order : 1; 30 | data.orderId = orderId ? orderId : max_order + 10; 31 | return await this.toolsModel.save(data); 32 | } 33 | 34 | async query(params) { 35 | const { page = 1, pageSize = 10, typeId, name } = params; 36 | const where: any = {}; 37 | typeId && (where.typeId = typeId); 38 | name && (where.name = Like(`%${name}%`)); 39 | const rows = await this.toolsModel.find({ 40 | order: { orderId: 'DESC' }, 41 | where, 42 | skip: (page - 1) * pageSize, 43 | take: pageSize, 44 | cache: true, 45 | }); 46 | const typeIds = rows.map((t) => t.typeId); 47 | const ToolsType = await this.toolsTypeModel.find({ id: In(typeIds) }); 48 | rows.forEach((t: any) => (t.toolName = ToolsType.find((k) => k.id == t.typeId)['name'])); 49 | const count = await this.toolsModel.count({ where }); 50 | return { rows, count }; 51 | } 52 | 53 | async del(params) { 54 | const { id } = params; 55 | return await this.toolsModel.delete({ id }); 56 | } 57 | 58 | /** 59 | * @desc 抖音视频去水印 拿无水印视频 音频 60 | * @param params {url: 视频地址} 61 | * @returns 62 | */ 63 | async douyin(params) { 64 | const { url } = params; 65 | const longUrl: any = await axios.get(url); 66 | const videoId = longUrl.request.path.substr(13, 19); 67 | const api = `https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${videoId}`; 68 | const res = await axios.get(api); 69 | const { music, video, share_info } = res.data.item_list[0]; 70 | const mp3 = music?.play_url?.uri; 71 | const mp4 = video.play_addr.url_list[0].replace('playwm', 'play'); 72 | const title = share_info.share_title; 73 | return { mp3, mp4, title }; 74 | } 75 | 76 | /** 77 | * @desc 抖音视频地址中转流文件进行下载 78 | * @param params {url: 视频地址} 79 | * @returns 80 | */ 81 | async douyinload(params, res) { 82 | const { url } = params; 83 | return axios({ 84 | url, 85 | headers: { 86 | accept: 87 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 88 | 'accept-language': 'en-US,en;q=0.9', 89 | 'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"', 90 | 'sec-ch-ua-mobile': '?0', 91 | 'sec-ch-ua-platform': '"macOS"', 92 | 'sec-fetch-dest': 'document', 93 | 'sec-fetch-mode': 'navigate', 94 | 'sec-fetch-site': 'none', 95 | 'sec-fetch-user': '?1', 96 | 'upgrade-insecure-requests': '1', 97 | cookie: 98 | 'msToken=xJsbor9nbwecJE3YIubD8KWOeRQl4k3LsBAQEcTNHfDNLo8Vtu1-93ZraUFaRoqOW6Dgo2r_KNh9gJKaEFtbn7-Z5q8HGe0PZO8ZQo54RZBZ', 99 | 'user-agent': 100 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36', 101 | }, 102 | responseType: 'stream', 103 | }).then((ret) => { 104 | const timespace = new Date().getTime(); 105 | res.header('Content-Disposition', `attachment; filename="snine-${timespace}.mp4"`); 106 | res.header('Content-type', `video/mp4`); 107 | ret.data.pipe(res); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/modules/type/dto/del.type.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsInt } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class TypeDelDto { 5 | @ApiProperty({ example: 1, description: '删除分类的id', name: '分类id' }) 6 | @IsInt({ message: '传入参数类型错误!' }) 7 | id: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/type/dto/set.type.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MaxLength } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class TypeSetDto { 5 | @ApiProperty({ example: 'Vue', description: '分类名称' }) 6 | @IsNotEmpty({ message: '分类名称不能为空' }) 7 | name: string; 8 | 9 | @ApiProperty({ example: 'vue', description: '分类英文简称,美化最终跳转路径' }) 10 | @IsNotEmpty({ message: '分类英文简称不能为空' }) 11 | value: string; 12 | 13 | @ApiProperty({ example: '这是vue汇总的分类', description: '关于分类的描述' }) 14 | @IsNotEmpty({ message: '描述为必填项!' }) 15 | @MaxLength(30, { message: '最多不超过30字!' }) 16 | desc: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/type/type.controller.ts: -------------------------------------------------------------------------------- 1 | import { TypeDelDto } from './dto/del.type.dto'; 2 | import { TypeSetDto } from './dto/set.type.dto'; 3 | import { TypeService } from './type.service'; 4 | import { Body, Query, Controller, Post, Get } from '@nestjs/common'; 5 | import { ApiTags } from '@nestjs/swagger'; 6 | @ApiTags('Type') 7 | @Controller('type') 8 | export class TypeController { 9 | constructor(private readonly typeService: TypeService) {} 10 | 11 | @Post('/set') 12 | set(@Body() params: TypeSetDto) { 13 | return this.typeService.set(params); 14 | } 15 | 16 | @Get('/query') 17 | query(@Query() params) { 18 | return this.typeService.query(params); 19 | } 20 | 21 | @Post('/del') 22 | del(@Body() params: TypeDelDto) { 23 | return this.typeService.del(params); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/type/type.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, 3 | Entity, 4 | // OneToMany, 5 | // OneToOne, 6 | // JoinColumn, 7 | } from 'typeorm'; 8 | // import { PhotoEntity } from '../photo/photo.entity'; 9 | import { BaseEntity } from 'src/common/entity/baseEntity'; 10 | 11 | @Entity({ name: 'tb_type' }) 12 | export class TypeEntity extends BaseEntity { 13 | @Column({ length: 16, unique: true }) 14 | name: string; 15 | 16 | @Column({ length: 16 }) 17 | value: string; 18 | 19 | @Column({ length: 30 }) 20 | desc: string; 21 | //一对多关系 22 | // @OneToMany(() => PhotoEntity, (photo) => photo.user) 23 | // photos: []; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/type/type.module.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { TypeEntity } from './type.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { TypeController } from './type.controller'; 6 | import { TypeService } from './type.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([TypeEntity, ArticleEntity])], 10 | controllers: [TypeController], 11 | providers: [TypeService], 12 | }) 13 | export class TypeModule {} 14 | -------------------------------------------------------------------------------- /src/modules/type/type.service.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { TypeEntity } from './type.entity'; 3 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 4 | import { InjectRepository } from '@nestjs/typeorm'; 5 | import { Repository } from 'typeorm'; 6 | 7 | @Injectable() 8 | export class TypeService { 9 | constructor( 10 | @InjectRepository(TypeEntity) 11 | private readonly TypeModel: Repository, 12 | @InjectRepository(ArticleEntity) 13 | private readonly ArticleModel: Repository, 14 | ) {} 15 | 16 | async set(params) { 17 | const { name, desc, value, id } = params; 18 | const type = await this.TypeModel.findOne({ name }); 19 | 20 | if (type) { 21 | return await this.TypeModel.update({ id: type.id }, { name, desc, value }); 22 | } 23 | if (id) { 24 | return await this.TypeModel.update({ id }, { name, desc, value }); 25 | } 26 | return await this.TypeModel.save({ name, desc, value }); 27 | } 28 | 29 | async query(params) { 30 | const { page = 1, pageSize = 10 } = params; 31 | const rows = await this.TypeModel.find({ 32 | order: { id: 'DESC' }, 33 | skip: (page - 1) * pageSize, 34 | take: pageSize, 35 | cache: true, 36 | }); 37 | const count = await this.TypeModel.count(); 38 | return { rows, count }; 39 | } 40 | 41 | async del(params) { 42 | const { id } = params; 43 | const count = await this.ArticleModel.count({ typeId: id }); 44 | if (count > 0) { 45 | throw new HttpException('该分类有文章正在使用中!', HttpStatus.BAD_REQUEST); 46 | } 47 | return await this.TypeModel.delete({ id }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { UploadService } from './upload.service'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { Body, Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common'; 4 | import { FileInterceptor } from '@nestjs/platform-express'; 5 | 6 | @ApiTags('Upload') 7 | @Controller('upload') 8 | export class UploadController { 9 | constructor(private readonly UploadService: UploadService) {} 10 | 11 | @Post('/file') 12 | @UseInterceptors(FileInterceptor('file')) 13 | upload(@UploadedFile() file) { 14 | return this.UploadService.upload(file); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/modules/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UploadController } from './upload.controller'; 3 | import { UploadService } from './upload.service'; 4 | 5 | @Module({ 6 | controllers: [UploadController], 7 | providers: [UploadService], 8 | }) 9 | export class UploadModule {} 10 | -------------------------------------------------------------------------------- /src/modules/upload/upload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { saveFile } from '../../lib/file'; 3 | 4 | @Injectable() 5 | export class UploadService { 6 | // TODO 目前上传均为公共文件,私有文件接口待开发 7 | async upload(file) { 8 | const { originalname: filename, buffer, size } = file; 9 | return await saveFile({ filename, buffer }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/user/dto/login.user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MinLength, MaxLength } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class UserLoginDto { 5 | @ApiProperty({ example: 'admin', description: '用户名', required: true }) 6 | @IsNotEmpty({ message: '用户名不能为空' }) 7 | username: string; 8 | 9 | @ApiProperty({ example: '123456', description: '密码', required: true }) 10 | @IsNotEmpty({ message: '密码不能为空' }) 11 | @MinLength(6, { message: '密码长度最低为6位' }) 12 | @MaxLength(30, { message: '密码长度最多为30位' }) 13 | password: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/user/dto/register.user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MinLength, MaxLength, IsEmail, IsOptional, IsEnum } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | import { Type } from 'class-transformer'; 4 | 5 | export class UserRegisterDto { 6 | @ApiProperty({ example: 'admin', description: '用户名' }) 7 | @IsNotEmpty({ message: '用户名不能为空' }) 8 | username: string; 9 | 10 | @ApiProperty({ example: '小九', description: '用户昵称' }) 11 | @IsNotEmpty({ message: '用户昵称不能为空' }) 12 | @MaxLength(8, { message: '用户昵称长度最多为8位' }) 13 | nickname: string; 14 | 15 | @ApiProperty({ example: '123456', description: '密码' }) 16 | @IsNotEmpty({ message: '密码不能为空' }) 17 | @MinLength(6, { message: '密码长度最低为6位' }) 18 | @MaxLength(30, { message: '密码长度最多为30位' }) 19 | password: string; 20 | 21 | @ApiProperty({ example: '123456@qq.com', description: '邮箱' }) 22 | @IsEmail({}, { message: '请填写正确格式的邮箱' }) 23 | email: string; 24 | 25 | @ApiProperty({ example: true, description: '是否不使用邮箱验证' }) 26 | isUseEmailVer: boolean; 27 | 28 | @ApiProperty({ example: 'https://www.xxxx.png', description: '头像', required: false }) 29 | avatar: string; 30 | 31 | @ApiProperty({ 32 | example: 1, 33 | description: '账号状态', 34 | required: false, 35 | enum: [1, 2], 36 | }) 37 | @IsOptional() 38 | @IsEnum([1, 2], { message: 'sex只能是1或者2' }) 39 | status: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/user/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy, ExtractJwt } from 'passport-jwt'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { secret } from 'src/config/jwt'; 5 | 6 | @Injectable() 7 | export class JwtStrategy extends PassportStrategy(Strategy) { 8 | constructor() { 9 | super({ 10 | jwtFromRequest: ExtractJwt.fromHeader('token'), 11 | ignoreExpiration: false, 12 | secretOrKey: secret, 13 | }); 14 | } 15 | 16 | async validate(payload: any) { 17 | return { 18 | userId: payload.userId, 19 | username: payload.username, 20 | email: payload.email, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/user/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { UserService } from 'src/modules/user/user.service'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 4 | import { Strategy } from 'passport-local'; 5 | 6 | @Injectable() 7 | export class LocalStrategy extends PassportStrategy(Strategy) { 8 | constructor(private readonly userService: UserService) { 9 | super(); 10 | } 11 | 12 | async validate(username: string, password: string): Promise { 13 | const user = await this.userService.validateUser(username, password); 14 | if (!user) { 15 | throw new HttpException( 16 | { message: '账号或者密码错误!', error: 'please try again later.' }, 17 | HttpStatus.BAD_REQUEST, 18 | ); 19 | } 20 | return user; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { ApiTags } from '@nestjs/swagger'; 2 | import { UserService } from './user.service'; 3 | import { Body, Controller, Post, Request, Get, Query } from '@nestjs/common'; 4 | import { UserRegisterDto } from './dto/register.user.dto'; 5 | import { UserLoginDto } from './dto/login.user.dto'; 6 | 7 | @Controller('/user') 8 | @ApiTags('User') 9 | export class UserController { 10 | constructor(private readonly userService: UserService) {} 11 | 12 | @Post('/register') 13 | register(@Body() params: UserRegisterDto) { 14 | return this.userService.register(params); 15 | } 16 | 17 | @Post('/login') 18 | login(@Body() params: UserLoginDto) { 19 | return this.userService.login(params); 20 | } 21 | 22 | @Get('/getInfo') 23 | queryInfo(@Request() req) { 24 | return this.userService.getInfo(req.payload); 25 | } 26 | 27 | @Get('/query') 28 | query(@Query() params) { 29 | return this.userService.query(params); 30 | } 31 | 32 | @Post('/update') 33 | update(@Request() req, @Body() params) { 34 | return this.userService.update(req.payload, params); 35 | } 36 | 37 | @Post('/updateUserInfo') 38 | updateUserInfo(@Request() req, @Body() params) { 39 | return this.userService.updateUserInfo(req.payload, params); 40 | } 41 | 42 | @Get('/userList') 43 | userList(@Query() params) { 44 | return this.userService.userList(params); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/modules/user/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; 3 | import { BaseEntity } from 'src/common/entity/baseEntity'; 4 | 5 | @Entity({ name: 'tb_user' }) //数据表的名字 6 | export class UserEntity extends BaseEntity { 7 | @Column({ length: 12 }) 8 | username: string; 9 | 10 | @Column({ length: 12 }) 11 | nickname: string; 12 | 13 | @Column({ length: 1000 }) 14 | password: string; 15 | 16 | @Column({ default: 1 }) 17 | status: number; 18 | 19 | @Column({ default: 1 }) 20 | sex: number; 21 | 22 | @Column({ nullable: true }) 23 | roomId: number; 24 | 25 | @Column({ length: 64, unique: true }) 26 | email: string; 27 | 28 | @Column({ length: 600, nullable: true }) 29 | avatar: string; 30 | 31 | @Column({ length: 10, default: 'viewer' }) 32 | role: string; 33 | 34 | @Column({ length: 255, nullable: true }) 35 | roomBg: string; 36 | 37 | @Column({ length: 255, default: '每个人都有签名、我希望你也有...' }) 38 | sign: string; 39 | 40 | @OneToMany(() => ArticleEntity, (article) => article.title) 41 | @JoinColumn({ name: 'nickname' }) 42 | articles: ArticleEntity[]; 43 | } 44 | -------------------------------------------------------------------------------- /src/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { VerifyEntity } from './../verify/verify.entity'; 2 | import { LocalStrategy } from './local.strategy'; 3 | import { JwtStrategy } from './jwt.strategy'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { UserEntity } from './user.entity'; 6 | import { TypeOrmModule } from '@nestjs/typeorm'; 7 | import { Module } from '@nestjs/common'; 8 | import { UserController } from './user.controller'; 9 | import { UserService } from './user.service'; 10 | import { PassportModule } from '@nestjs/passport'; 11 | import { expiresIn, secret } from 'src/config/jwt'; 12 | 13 | @Module({ 14 | imports: [ 15 | PassportModule, 16 | JwtModule.register({ 17 | secret, 18 | signOptions: { expiresIn }, 19 | }), 20 | TypeOrmModule.forFeature([UserEntity, VerifyEntity]), 21 | ], 22 | controllers: [UserController], 23 | providers: [UserService, LocalStrategy, JwtStrategy], 24 | }) 25 | export class UserModule {} 26 | -------------------------------------------------------------------------------- /src/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { MailerService } from '@nest-modules/mailer'; 2 | import { VerifyEntity } from './../verify/verify.entity'; 3 | import { randomAvatar } from './../../constant/avatar'; 4 | import { UserEntity } from './user.entity'; 5 | import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; 6 | import { hashSync, compareSync } from 'bcryptjs'; 7 | import { InjectRepository } from '@nestjs/typeorm'; 8 | import { Repository, In, Like } from 'typeorm'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { randomCode } from 'src/utils/tools'; 11 | 12 | @Injectable() 13 | export class UserService { 14 | constructor( 15 | @InjectRepository(UserEntity) 16 | private readonly UserModle: Repository, 17 | @InjectRepository(VerifyEntity) 18 | private readonly VerifyModle: Repository, 19 | private readonly jwtService: JwtService, 20 | private readonly mailerService: MailerService, 21 | ) {} 22 | 23 | /** 24 | * @desc 账号注册 25 | * @param params 26 | * @param isUseEmailVer 是否使用邮箱验证由用户决定 否则验证过于繁琐 强制验证则前端不传这个参数即可 true:不验证 false:验证 27 | * @returns 28 | */ 29 | async register(params) { 30 | const { username, password, email, avatar, isUseEmailVer } = params; 31 | params.password = hashSync(password); 32 | if (!avatar) { 33 | params.avatar = randomAvatar(); 34 | } 35 | const u: any = await this.UserModle.findOne({ 36 | where: [{ username }, { email }], 37 | }); 38 | if (u) { 39 | const tips = username == u.username ? '用户名' : '邮箱'; 40 | throw new HttpException({ message: `该${tips}已经存在了!` }, HttpStatus.BAD_REQUEST); 41 | } 42 | !isUseEmailVer && (params.status = 0); // 使用邮箱验证的情况 默认账号是冻结状态 43 | const newUser = await this.UserModle.save(params); 44 | /* 需要邮箱验证 */ 45 | if (!isUseEmailVer) { 46 | const code = randomCode(8); 47 | const expirationTime = (new Date().getTime() + 60 * 5 * 1000).toString(); 48 | const params = { code, email, type: 'register', expirationTime }; 49 | await this.VerifyModle.save(params); 50 | const baseApi = 'https://api.jiangly.com'; // 暂时不加测试环境 51 | await this.mailerService.sendMail({ 52 | to: email, 53 | from: 'Nine_Team@163.com', 54 | subject: '来自Nine聊天室的注册验证', 55 | html: ` 56 | Nine Team 邮箱验证码 57 |

请点下以下链接激活您的账号,点此激活您的账号

58 | 来自 --小九的博客 59 |
`, 60 | template: './welcome', 61 | // context: { 62 | // // url: '', 63 | // }, 64 | }); 65 | return '请前往邮箱激活您的账号用户登录系统!'; 66 | } else { 67 | return newUser; 68 | } 69 | } 70 | 71 | /** 72 | * @desc 账号登录 73 | * @param params 74 | * @returns 75 | */ 76 | async login(params): Promise { 77 | const { username, password } = params; 78 | const u: any = await this.UserModle.findOne({ 79 | where: [{ username }, { email: username }], 80 | }); 81 | if (!u) { 82 | throw new HttpException('该用户不存在!', HttpStatus.BAD_REQUEST); 83 | } 84 | const bool = await compareSync(password, u.password); 85 | if (bool && u.status === 1) { 86 | const { username, email, id: userId, role, nickname, avatar, sign, roomBg, roomId } = u; 87 | return { 88 | token: this.jwtService.sign({ username, nickname, email, userId, role, avatar, sign, roomBg, roomId }), 89 | }; 90 | } else { 91 | throw new HttpException( 92 | { message: '账号密码错误、或账号未激活!', error: 'please try again later.' }, 93 | HttpStatus.BAD_REQUEST, 94 | ); 95 | } 96 | } 97 | 98 | async getInfo(payload) { 99 | const { userId: id, exp: failure_time } = payload; 100 | const u = await this.UserModle.findOne({ 101 | where: { id }, 102 | select: ['username', 'nickname', 'email', 'avatar', 'role', 'sign', 'roomBg', 'roomId'], 103 | }); 104 | return { userInfo: Object.assign(u, { userId: id }), failure_time }; 105 | } 106 | 107 | /** 108 | * @desc 密码校验,用于local策略,现在使用全局AuthGuard,暂时不用 109 | * @param username 110 | * @param password 111 | * @returns 112 | */ 113 | async validateUser(username: string, password: string) { 114 | const u: any = await this.UserModle.findOne({ 115 | where: { username }, 116 | }); 117 | const bool = compareSync(password, u.password); 118 | if (bool) { 119 | return u; 120 | } else { 121 | return false; 122 | } 123 | } 124 | 125 | async query(params) { 126 | const { page = 1, pageSize = 10, role } = params; 127 | const where: any = {}; 128 | role && (where.role = In(role)); 129 | const rows = await this.UserModle.find({ 130 | order: { id: 'DESC' }, 131 | where, 132 | skip: (page - 1) * pageSize, 133 | take: pageSize, 134 | cache: true, 135 | select: ['id', 'nickname'], 136 | }); 137 | const count = await this.UserModle.count(); 138 | return { rows, count }; 139 | } 140 | 141 | async update(payload, params) { 142 | const { userId: id } = payload; 143 | const { avatar, username, nickname, roomBg, sign } = params; 144 | const upateInfoData: any = {}; 145 | avatar && (upateInfoData.avatar = avatar); 146 | username && (upateInfoData.username = username); 147 | nickname && (upateInfoData.nickname = nickname); 148 | roomBg && (upateInfoData.roomBg = roomBg); 149 | sign && (upateInfoData.sign = sign); 150 | await this.UserModle.update({ id }, upateInfoData); 151 | return '修改成功'; 152 | } 153 | 154 | async userList(params) { 155 | const { page = 1, pageSize = 10, role, status, keyword } = params; 156 | const basicWhere: any = {}; 157 | status && (basicWhere.status = status); 158 | status === 0 && (basicWhere.status = status); 159 | role && (basicWhere.role = role); 160 | let where: any = []; 161 | /* 关键词查询多个类型 或的关系 */ 162 | if (keyword) { 163 | where.push({ ...basicWhere, ...{ username: Like(`%${keyword}%`) } }); 164 | where.push({ ...basicWhere, ...{ nickname: Like(`%${keyword}%`) } }); 165 | where.push({ ...basicWhere, ...{ email: Like(`%${keyword}%`) } }); 166 | } else { 167 | where = basicWhere; 168 | } 169 | 170 | const rows = await this.UserModle.find({ 171 | where, 172 | order: { id: 'DESC' }, 173 | skip: (page - 1) * pageSize, 174 | take: pageSize, 175 | cache: true, 176 | }); 177 | const count = await this.UserModle.count({ where }); 178 | return { rows, count }; 179 | } 180 | 181 | async updateUserInfo(payload, params) { 182 | const roleGradeMap = { 183 | admin: 1, 184 | super: 2, 185 | guest: 3, 186 | viewer: 4, 187 | }; 188 | const { id, status, email, username, nickname, sex, role, sign } = params; 189 | const { role: mineRole, userId: mineId } = payload; 190 | const updateUserInfo = await this.UserModle.findOne({ id }); 191 | /* 自己修改自己信息的话,其他可以修改 权限不能给个提示 */ 192 | if (mineId === id && role) { 193 | throw new HttpException( 194 | { message: '不要尝试修改自己的权限等级哦', error: 'please try again later.' }, 195 | HttpStatus.BAD_REQUEST, 196 | ); 197 | } 198 | /* 数字越小等级越大 */ 199 | if (roleGradeMap[updateUserInfo.role] <= roleGradeMap[mineRole]) { 200 | throw new HttpException( 201 | { message: '您无权操作和你同级或高于您等级的用户!', error: 'please try again later.' }, 202 | HttpStatus.BAD_REQUEST, 203 | ); 204 | } 205 | const updateInfo: any = {}; 206 | status && (updateInfo.status = status); 207 | status === 0 && (updateInfo.status = status); 208 | email && (updateInfo.email = email); 209 | username && (updateInfo.username = username); 210 | nickname && (updateInfo.nickname = nickname); 211 | sex && (updateInfo.sex = sex); 212 | role && (updateInfo.role = role); 213 | sign && (updateInfo.sign = sign); 214 | const { affected } = await this.UserModle.update({ id }, updateInfo); 215 | if (affected > 0) { 216 | return '修改成功'; 217 | } else { 218 | throw new HttpException( 219 | { message: '修改用户信息失败', error: 'please try again later.' }, 220 | HttpStatus.BAD_REQUEST, 221 | ); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/modules/verify/dto/login.user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, MinLength, MaxLength } from 'class-validator'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class AccountActiveDto { 5 | @ApiProperty({ example: '000000', description: '验证码', required: true }) 6 | @IsNotEmpty({ message: '验证码不能为空' }) 7 | code: number; 8 | 9 | @ApiProperty({ example: '123456@qq.com', description: '验证的邮箱', required: true }) 10 | @IsNotEmpty({ message: '邮箱不能为空' }) 11 | email: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/verify/verify.controller.ts: -------------------------------------------------------------------------------- 1 | import { AccountActiveDto } from './dto/login.user.dto'; 2 | import { VerifyService } from './verify.service'; 3 | import { Controller, Get, Query, Render, Response } from '@nestjs/common'; 4 | import { ApiTags } from '@nestjs/swagger'; 5 | 6 | @ApiTags('Verify') 7 | @Controller('verify') 8 | export class VerifyController { 9 | constructor(private readonly VerifyService: VerifyService) {} 10 | 11 | @Get('/accountActive') 12 | accountActive(@Query() params: AccountActiveDto, @Response() res) { 13 | return this.VerifyService.accountActive(params, res); 14 | } 15 | 16 | @Get('/verifySuccess') 17 | @Render('account/verify_success') 18 | verifySuccess(@Query() params) { 19 | const { nickname, count } = params; 20 | return { nickname, count }; 21 | } 22 | 23 | @Get('/verifyError') 24 | @Render('account/verify_error') 25 | verifyError(@Query() params) { 26 | return {}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/verify/verify.entity.ts: -------------------------------------------------------------------------------- 1 | import { ArticleEntity } from './../article/article.entity'; 2 | import { Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm'; 3 | import { BaseEntity } from 'src/common/entity/baseEntity'; 4 | 5 | @Entity({ name: 'tb_verify' }) 6 | export class VerifyEntity extends BaseEntity { 7 | @Column({ length: 30 }) 8 | code: string; 9 | 10 | @Column({ length: 64 }) 11 | email: string; 12 | 13 | @Column({ default: 0 }) 14 | errorNum: number; 15 | 16 | @Column({ length: 64 }) 17 | expirationTime: string; 18 | 19 | @Column({ length: 30 }) 20 | type: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/verify/verify.module.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { VerifyEntity } from './verify.entity'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { Module } from '@nestjs/common'; 5 | import { VerifyController } from './verify.controller'; 6 | import { VerifyService } from './verify.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forFeature([VerifyEntity, UserEntity])], 10 | controllers: [VerifyController], 11 | providers: [VerifyService], 12 | }) 13 | export class VerifyModule {} 14 | -------------------------------------------------------------------------------- /src/modules/verify/verify.service.ts: -------------------------------------------------------------------------------- 1 | import { UserEntity } from './../user/user.entity'; 2 | import { VerifyEntity } from './verify.entity'; 3 | import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; 4 | import { Repository } from 'typeorm'; 5 | import { InjectRepository } from '@nestjs/typeorm'; 6 | 7 | @Injectable() 8 | export class VerifyService { 9 | constructor( 10 | @InjectRepository(VerifyEntity) 11 | private readonly VerifyModle: Repository, 12 | @InjectRepository(UserEntity) 13 | private readonly UserModle: Repository, 14 | ) {} 15 | 16 | async accountActive(params, res) { 17 | const { code, email } = params; 18 | const verify = await this.VerifyModle.findOne({ 19 | where: { code, email }, 20 | }); 21 | const user = await this.UserModle.findOne({ where: { email } }); 22 | const { nickname, id } = user; 23 | if (!verify) { 24 | await this.UserModle.delete({ id }); 25 | res.redirect(`/api/verify/verifyError`); 26 | // throw new HttpException("激活失败、检查您的邮箱和验证码!", HttpStatus.BAD_REQUEST) 27 | } else { 28 | const { expirationTime } = verify; 29 | const now = new Date().getTime(); 30 | 31 | const isExpire = now - Number(expirationTime) < 0; 32 | if (isExpire) { 33 | await this.UserModle.update({ id }, { status: 1 }); 34 | res.redirect(`/api/verify/verifySuccess?nickname=${nickname}&count=${id}`); 35 | } else { 36 | await this.UserModle.delete({ id }); 37 | res.redirect(`/api/verify/verifyError`); 38 | // throw new HttpException("您的验证码已过期、请重新注册!", HttpStatus.BAD_REQUEST) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/swagger/index.ts: -------------------------------------------------------------------------------- 1 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 2 | 3 | const swaggerOptions = new DocumentBuilder() 4 | .setTitle('snine blog api document') 5 | .setDescription('about snine blog api docs') 6 | .setVersion('1.0.0') 7 | .addBearerAuth() 8 | .build(); 9 | 10 | export function createSwagger(app) { 11 | const document = SwaggerModule.createDocument(app, swaggerOptions); 12 | SwaggerModule.setup('/docs', app, document); 13 | } 14 | -------------------------------------------------------------------------------- /src/tasks/tasks.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TasksService } from './tasks.service'; 3 | 4 | @Module({ 5 | providers: [TasksService], 6 | }) 7 | export class TasksModule {} 8 | -------------------------------------------------------------------------------- /src/tasks/tasks.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Cron, Interval, Timeout } from '@nestjs/schedule'; 3 | 4 | @Injectable() 5 | export class TasksService { 6 | private readonly logger = new Logger(TasksService.name); 7 | 8 | /** 9 | * @desc 每晚更新一次热门音乐列表,每天更新30首用于chat大厅播放 10 | */ 11 | // @Cron('30 * * * * *') 12 | // handleCron() { 13 | // this.logger.debug('Called when the second is 45'); 14 | // } 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/email/welcome.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | title Nine邮箱注册 5 | body 6 | h1 Welcome to register. 7 | #container.col 8 | p 欢迎加入Nine聊天室 9 | p 请点击下方链接激活您的账号 10 | ul 11 | li 百度 #{url} 12 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | export const formatDate = (dateNum: string | number): string => { 4 | return moment(dateNum).format('YYYY-MM-DD HH:mm:ss'); 5 | }; 6 | 7 | /** 8 | * @desc 格式化百度统计请求入参 9 | * @param dateNum 10 | * @returns 11 | */ 12 | export const formatBaiduReq = (dateNum: string | number): string => { 13 | return moment(dateNum).format('YYYYMMDD'); 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './date'; 2 | -------------------------------------------------------------------------------- /src/utils/spider.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from 'cheerio'; 2 | import axios from 'axios'; 3 | import * as Qs from 'qs'; 4 | 5 | import html2md from 'html-to-md'; 6 | 7 | /** 8 | * @desc 请求页面通过cherrion格式化文档返回给业务处理 9 | * @param url 请求地址 10 | * @returns 11 | */ 12 | export const requestHtml = async (url) => { 13 | const body: any = await requestInterface(url); 14 | return cheerio.load(body, { decodeEntities: false }); 15 | }; 16 | 17 | /** 18 | * @desc 提取出博客的正文html部分转为md格式 19 | * @param html 解析的网页 20 | * @param dom 需要分析的dom节点 21 | * @returns 22 | */ 23 | export const formatHtml = (html, dom = '.markdown-body') => { 24 | const $ = cheerio.load(html, { decodeEntities: false }); 25 | const data = $(dom).html(); 26 | return html2md(data); 27 | }; 28 | 29 | /** 30 | * @desc axios调用三方接口使用 31 | */ 32 | export const requestInterface = async (url, param = {}, method: any = 'GET') => { 33 | return new Promise((resolve, reject) => { 34 | axios({ 35 | method, 36 | headers: { 37 | accept: 'application/json, text/plain, */*', 38 | 'accept-language': 'zh-CN,zh;q=0.9', 39 | 'cache-control': 'no-cache', 40 | csrf: 'MEWX5B55MBB', 41 | pragma: 'no-cache', 42 | 'sec-ch-ua': '"Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"', 43 | 'sec-ch-ua-mobile': '?0', 44 | 'sec-ch-ua-platform': '"macOS"', 45 | 'sec-fetch-dest': 'empty', 46 | 'sec-fetch-mode': 'cors', 47 | 'sec-fetch-site': 'same-origin', 48 | cookie: 49 | '_ga=GA1.2.1049405325.1635954830; _gid=GA1.2.2140587553.1635954830; Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1635954830,1636115008; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1636170510; _gat=1; kw_token=MEWX5B55MBB', 50 | }, 51 | url, 52 | data: Qs.stringify(param), 53 | }) 54 | .then((res) => { 55 | resolve(res.data); 56 | }) 57 | .catch((err) => { 58 | reject(err); 59 | }); 60 | }); 61 | }; 62 | 63 | /** 64 | * @desc 搜索音乐 65 | * @param url 66 | * @returns 67 | */ 68 | export const searchMusic = async (url) => { 69 | return new Promise((resolve, reject) => { 70 | axios({ 71 | url, 72 | method: 'GET', 73 | headers: { 74 | accept: 'application/json, text/plain, */*', 75 | 'accept-language': 'zh-CN,zh;q=0.9', 76 | 'sec-ch-ua': '"Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"', 77 | 'sec-ch-ua-mobile': '?0', 78 | 'sec-ch-ua-platform': '"macOS"', 79 | 'sec-fetch-dest': 'empty', 80 | 'sec-fetch-mode': 'cors', 81 | 'sec-fetch-site': 'same-origin', 82 | Referer: 'https://www.kuwo.cn/search/list?key=%E5%AD%A4%E5%9F%8E', 83 | Cookie: 84 | '_ga=GA1.2.1049405325.1635954830; _gid=GA1.2.2140587553.1635954830; gid=bc653d0b-61c5-4d69-ac7f-2ee3e87dd0ce; Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1635954830,1636115008,1636207872; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1636211110; kw_token=H77LBB1XLI; _gat=1', 85 | csrf: 'H77LBB1XLI', 86 | }, 87 | }) 88 | .then((res) => { 89 | resolve(res.data); 90 | }) 91 | .catch((err) => { 92 | reject(err); 93 | }); 94 | }); 95 | }; 96 | 97 | /** 98 | * @desc 获取酷我音乐单个分类下的音乐的30首列表并写入数据库 99 | * 每次拿到目前数据库最大的customId往上加 100 | * @returns [] 歌曲列表 101 | */ 102 | export const spiderKuWoHotMusic = async (url, lastMaxCustomId = 0) => { 103 | const body: any = await requestInterface(url); 104 | const $ = cheerio.load(body, { decodeEntities: false }); 105 | const musicListNodes = $('.album_list:first').children(); 106 | const musicList = []; 107 | musicListNodes.each((index, node) => { 108 | const customId = index + 1 + lastMaxCustomId; 109 | const href = $(node).find('a').attr('href'); 110 | const mid = href.split('/')[2]; 111 | const album = $(node).find('a').attr('title'); 112 | const time = $(node).find('.song_time span').text(); 113 | const duration = Number(time.split(':')[0]) * 60 + Number(time.split(':')[1]); 114 | const singer = $(node).find('.song_artist span').text(); 115 | musicList.push({ customId, album, duration, singer, mid }); 116 | }); 117 | return musicList; 118 | }; 119 | 120 | /* mp4获取地址 */ 121 | // https://www.kuwo.cn/api/v1/www/music/playUrl?mid=187717013&type=mv&httpsStatus=1&reqId=958f2950-3f25-11ec-a345-4125bd2a21d6 122 | 123 | /** 124 | * @desc 获取百度统计的数据 125 | * @param url 126 | * @returns 127 | */ 128 | export const searchBaiduData = async (url = 'https://openapi.baidu.com/rest/2.0/tongji/report/getData') => { 129 | return new Promise((resolve, reject) => { 130 | axios 131 | .get( 132 | 'https://openapi.baidu.com/rest/2.0/tongji/report/getData?access_token=121.141b90164ddd132df0ebb1ce4c60eb07.Y3dQBY0NoH2Llf7M_1Zub-guF8V5AeWpwIDpFMp.48jxLQ&site_id=17558179&method=overview/getTimeTrendRpt&start_date=20220111&end_date=20220122&metrics=pv_count,visitor_count,ip_count', 133 | ) 134 | .then((res) => { 135 | resolve(res.data); 136 | }); 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /src/utils/tools.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | 3 | export const https_get = async (url) => { 4 | let pipe = null; 5 | return new Promise((resovle, reject) => { 6 | https.get(url, (res) => { 7 | if (res.statusCode == 200) { 8 | res.on('data', (chunk) => { 9 | pipe += chunk; 10 | }); 11 | res.on('end', () => { 12 | const { data } = JSON.parse(pipe.split('null')[1]); 13 | if (data.length) { 14 | const { origip, location } = data[0]; 15 | resovle({ ip: origip, address: location.split(' ')[0] }); 16 | } else { 17 | reject('获取ip失败'); 18 | } 19 | }); 20 | } else { 21 | reject('获取ip失败'); 22 | } 23 | }); 24 | }); 25 | }; 26 | 27 | /** 28 | * @desc 转会数据格式,把映射对象转为数组 并把房主放到首位 29 | * @param onlineUserInfo 30 | * @returns 31 | */ 32 | export const formatOnlineUser = (onlineUserInfo = {}) => { 33 | const keys = Object.keys(onlineUserInfo); 34 | if (!keys.length) return []; 35 | let userInfo = Object.values(onlineUserInfo); 36 | let homeowner = null; 37 | const homeownerIndex = userInfo.findIndex((k: any) => k.role === 'admin'); 38 | homeownerIndex != -1 && (homeowner = userInfo.splice(homeownerIndex, 1)); 39 | homeownerIndex != -1 && (userInfo = [...homeowner, ...userInfo]); 40 | return userInfo; 41 | }; 42 | 43 | /** 44 | * @desc 延迟请求,爬虫遍历请求过快会导致被拉黑 45 | * @param time 时间 ms 46 | * @returns null 47 | */ 48 | export const delayRequest = (time) => { 49 | return new Promise((resolve) => { 50 | setTimeout(() => { 51 | resolve(true); 52 | }, time); 53 | }); 54 | }; 55 | 56 | /** 57 | * @desc 当前当前时间戳/1000 按秒计算 58 | * @params lastTimespace 传入上次时间戳则计算时间与现在的插值 59 | * @returns 60 | */ 61 | export const getTimeSpace = (lastTimespace = 0) => { 62 | const nowSpace = Math.round(new Date().getTime() / 1000); 63 | return lastTimespace ? nowSpace - lastTimespace : nowSpace; 64 | }; 65 | 66 | /** 67 | * @desc 随机生成验证码 68 | * @param len 验证码长度 69 | * @returns 70 | */ 71 | export const randomCode = (len = 6) => { 72 | let code = ''; 73 | for (let i = 0; i < len; i++) { 74 | const radom = Math.floor(Math.random() * 10); 75 | code += radom; 76 | } 77 | return code; 78 | }; 79 | 80 | /** 81 | * @desc 去除空参数,返回正确查询条件 82 | * @param arg 83 | */ 84 | export const delEmptyCondition = (arg) => { 85 | const result: any = {}; 86 | Object.keys(arg).forEach((key) => { 87 | arg[key] && (result[key] = arg[key]); 88 | }); 89 | return result; 90 | }; 91 | 92 | /** 93 | * @desc 取到其中唯一一个非空值的key 94 | * @param arg 95 | */ 96 | export const getNotEmptyKey = (arg) => { 97 | let result = null; 98 | Object.keys(arg).forEach((key) => { 99 | arg[key] && (result = key); 100 | }); 101 | return result; 102 | }; 103 | -------------------------------------------------------------------------------- /src/utils/verifyToken.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { HttpException, HttpStatus } from '@nestjs/common'; 3 | import { secret as key } from 'src/config/jwt'; 4 | 5 | /** 6 | * @desc 解析token 7 | * @param token 8 | * @param secret 9 | * @returns 10 | */ 11 | export function verifyToken(token, secret: string = key): Promise { 12 | return new Promise((resolve) => { 13 | jwt.verify(token, secret, (error, payload) => { 14 | if (error) { 15 | // throw new HttpException('身份验证失败', HttpStatus.UNAUTHORIZED); 16 | resolve({ userId: -1 }); 17 | } else { 18 | resolve(payload); 19 | } 20 | }); 21 | }); 22 | } 23 | 24 | /** 25 | * @desc 解析token 26 | * @param token 27 | * @param secret 28 | * @returns 29 | */ 30 | export function verifyPublicToken(token: string, secret: string = key): Promise { 31 | return new Promise((resolve) => { 32 | jwt.verify(token, secret, (error, payload) => { 33 | if (error) { 34 | throw new HttpException('身份验证失败', HttpStatus.UNAUTHORIZED); 35 | } else { 36 | resolve(payload); 37 | } 38 | }); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /utils/Result.js: -------------------------------------------------------------------------------- 1 | const { CODE_ERROR, CODE_SUCCESS, CODE_TOKEN_EXPIRED, CODE_REPEAT, CODE_WARNING } = require('../setting/constant') 2 | 3 | 4 | /** 5 | * 统一封装返回值的格式 6 | */ 7 | 8 | 9 | class Result { 10 | constructor ( data, msg = '操作成功', options) { 11 | this.data = null 12 | if(arguments.length === 0){ 13 | this.msg = '操作成功' 14 | } else if (arguments.length === 1) { 15 | this.msg = data 16 | } else { 17 | this.data = data 18 | this.msg = msg 19 | if(options) { 20 | this.options = options 21 | } 22 | } 23 | } 24 | 25 | createResult() { 26 | if (!this.code) { 27 | this.code = CODE_SUCCESS 28 | } 29 | let base = { 30 | code: this.code, 31 | msg: this.msg 32 | } 33 | 34 | if (this.data) { 35 | base.data = this.data 36 | } 37 | 38 | if (this.options) { 39 | base = { ...base, ...this.options} 40 | } 41 | return base; 42 | } 43 | 44 | json(res) { 45 | res.json(this.createResult()) 46 | } 47 | 48 | success(res) { 49 | this.code = CODE_SUCCESS 50 | this.json(res) 51 | } 52 | 53 | fail(res) { 54 | this.code = CODE_ERROR 55 | this.json(res) 56 | } 57 | 58 | warning(res) { 59 | this.code = CODE_WARNING 60 | this.json(res) 61 | } 62 | 63 | repeat(res){ 64 | this.code = CODE_REPEAT 65 | this.json(res) 66 | } 67 | 68 | jwtError(res) { 69 | this.code = CODE_TOKEN_EXPIRED 70 | this.json(res) 71 | } 72 | } 73 | 74 | module.exports = Result -------------------------------------------------------------------------------- /views/account/verify_error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 27 | 28 |
29 |

抱歉、账户激活失败

30 |

您未在规定的时间内激活账户、我们已清空您的注册信息、请您重新注册账户、谢谢!点击前往首页

31 |
32 | 33 | -------------------------------------------------------------------------------- /views/account/verify_success.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 27 | 28 |
29 |

恭喜、成功激活了您的账户

30 |

<%= data.nickname %> 欢迎加入小九的BLOG、你是本站的第 <%= data.count %>位用户、点击前往登录

31 |
32 | 33 | --------------------------------------------------------------------------------