├── .gitignore ├── .snyk ├── LICENSE ├── README.md ├── docs ├── _sidebar.md ├── config.md ├── environment.md ├── gitclone.md ├── guide.md ├── install.md ├── start.md └── techniques.md ├── lerna.json ├── nodemon-debug.json ├── nodemon.json ├── ormconfig.js ├── package-lock.json ├── package.json ├── renovate.json ├── src ├── cms │ ├── cms.module.ts │ ├── entities │ │ ├── article.entity.ts │ │ ├── classify.entity.ts │ │ ├── content.entity.ts │ │ ├── index.entity.ts │ │ ├── page-sort.entity.ts │ │ ├── page.entity.ts │ │ ├── picture-group.entity.ts │ │ ├── picture.entity.ts │ │ └── user-message.entity.ts │ ├── graphqls │ │ ├── article.types.graphql │ │ ├── classify.types.graphql │ │ ├── index.types.graphql │ │ ├── page-sort.types.graphql │ │ ├── page.types.graphql │ │ ├── picGroup.types.graphql │ │ ├── picture.types.graphql │ │ └── user-message.types.graphql │ ├── index.ts │ ├── interfaces │ │ ├── article.interface.ts │ │ ├── classify.interface.ts │ │ ├── page-sort.interface.ts │ │ └── page.interface.ts │ ├── resolvers │ │ ├── article.resolver.ts │ │ ├── classify.resolver.ts │ │ ├── index.resolver.ts │ │ ├── page-sort.resolver.ts │ │ ├── page.resolver.ts │ │ ├── pic-group.resolver.ts │ │ └── user-message.resolver.ts │ └── services │ │ ├── article.service.ts │ │ ├── classify.service.ts │ │ ├── index.service.ts │ │ ├── page-sort.service.ts │ │ ├── page.service.ts │ │ ├── pic-group.service.ts │ │ └── user-message.service.ts ├── interceptors │ └── errors.interceptor.ts ├── tsconfig.json └── user │ ├── auth │ └── auth.service.ts │ ├── constants │ └── auth.constant.ts │ ├── entities │ ├── role.entity.ts │ └── user.entity.ts │ ├── graphqls │ ├── common.types.graphql │ ├── role.types.graphql │ └── user.types.graphql │ ├── i18n │ ├── en-US.json │ └── zh-CN.json │ ├── index.ts │ ├── interfaces │ ├── common-result.interface.ts │ ├── jwt.interface.ts │ ├── role.interface.ts │ └── user.interface.ts │ ├── resolvers │ ├── role.resolver.ts │ └── user.resolver.ts │ ├── services │ ├── entity-check.service.ts │ ├── role.service.ts │ └── user.service.ts │ ├── user.module.ts │ └── utils │ ├── crypto.util.ts │ └── pager.util.ts ├── starter ├── app.module.ts ├── graphql.config.ts ├── main.ts └── tsconfig.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.gitignore.io/api/linux,visualstudiocode,node 4 | # Edit at https://www.gitignore.io/?templates=linux,visualstudiocode,node 5 | 6 | ### Linux ### 7 | *~ 8 | 9 | # temporary files which can be created if a process still has a handle open of a deleted file 10 | .fuse_hidden* 11 | 12 | # KDE directory preferences 13 | .directory 14 | 15 | # Linux trash folder which might appear on any partition or disk 16 | .Trash-* 17 | 18 | # .nfs files are created when an open file is removed but is still being accessed 19 | .nfs* 20 | 21 | ### Node ### 22 | # Logs 23 | logs 24 | *.log 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # Runtime data 30 | pids 31 | *.pid 32 | *.seed 33 | *.pid.lock 34 | 35 | # Directory for instrumented libs generated by jscoverage/JSCover 36 | lib-cov 37 | 38 | # Coverage directory used by tools like istanbul 39 | coverage 40 | 41 | # nyc test coverage 42 | .nyc_output 43 | 44 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 45 | .grunt 46 | 47 | # Bower dependency directory (https://bower.io/) 48 | bower_components 49 | 50 | # node-waf configuration 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | build/Release 55 | 56 | # Dependency directories 57 | node_modules/ 58 | jspm_packages/ 59 | 60 | # TypeScript v1 declaration files 61 | typings/ 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | .env.test 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | 85 | # next.js build output 86 | .next 87 | 88 | # nuxt.js build output 89 | .nuxt 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | ### VisualStudioCode ### 104 | .vscode/* 105 | !.vscode/settings.json 106 | !.vscode/tasks.json 107 | !.vscode/launch.json 108 | !.vscode/extensions.json 109 | 110 | ### VisualStudioCode Patch ### 111 | # Ignore all local history of files 112 | .history 113 | 114 | # End of https://www.gitignore.io/api/linux,visualstudiocode,node 115 | 116 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 117 | 118 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.13.5 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-AXIOS-174505: 7 | - '@nestjs/common > axios': 8 | patched: '2019-05-06T06:55:38.605Z' 9 | SNYK-JS-LODASH-450202: 10 | - snyk > inquirer > lodash: 11 | patched: '2019-07-04T06:54:16.366Z' 12 | - snyk > lodash: 13 | patched: '2019-07-04T06:54:16.366Z' 14 | - snyk > snyk-php-plugin > lodash: 15 | patched: '2019-07-04T06:54:16.366Z' 16 | - snyk > snyk-nuget-plugin > lodash: 17 | patched: '2019-07-04T06:54:16.366Z' 18 | - snyk > snyk-nodejs-lockfile-parser > lodash: 19 | patched: '2019-07-04T06:54:16.366Z' 20 | - snyk > @snyk/dep-graph > lodash: 21 | patched: '2019-07-04T06:54:16.366Z' 22 | - '@nestjs/graphql > lodash': 23 | patched: '2019-07-04T06:54:16.366Z' 24 | - snyk > snyk-config > lodash: 25 | patched: '2019-07-04T06:54:16.366Z' 26 | - snyk > snyk-mvn-plugin > lodash: 27 | patched: '2019-07-04T06:54:16.366Z' 28 | - snyk > snyk-nodejs-lockfile-parser > graphlib > lodash: 29 | patched: '2019-07-04T06:54:16.366Z' 30 | - snyk > snyk-go-plugin > graphlib > lodash: 31 | patched: '2019-07-04T06:54:16.366Z' 32 | - snyk > @snyk/dep-graph > graphlib > lodash: 33 | patched: '2019-07-04T06:54:16.366Z' 34 | - '@nestjs/core > @nuxtjs/opencollective > consola > lodash': 35 | patched: '2019-07-04T06:54:16.366Z' 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notadd CMS 2 | 3 | ## 特性 4 | 5 | - 现代化管理面板(可选) 6 | - AOP 面向切面编程,基于 Nest.js 7 | - 便于维护,基于 Typescript 8 | - 高性能 【异步IO,单机并发1W+】 9 | - 不限于前端,支持 Vue(推荐Nuxt)、Angular 、React 10 | - 强大的 API: 支持 Graphql 11 | - 优雅的 ORM : 基于Typeorm 12 | 13 | ## 功能 14 | 15 | - [x] 文章分类管理 16 | - [x] 文章列表查询 17 | - [x] 文章批量审核 18 | - [x] 文章批量删除 19 | - [x] 页面分类管理 20 | - [x] 页面管理 21 | - [x] 轮播图管理 22 | 23 | 24 | ## 环境要求: 25 | 26 | Nodejs: 8+ 27 | 28 | 数据库: PostgreSQL 9.5+, MariaDB 10.2+, Mysql 5.7+, SQLite, Mongodb, MS SQL Server, Oracle (任意一种) 29 | 30 | [1分钟安装环境](#install) 31 | 32 | ## 使用说明 33 | 34 | gitclone源码; 35 | - `git clone https://github.com/notadd/nt-cms.git` 36 | 37 | 进入项目目录下安装项目依赖 38 | 39 | - `npm install` 40 | 41 | 创建项目需要的数据库并在`ormconfig.js`文件中配置数据库连接; 42 | 43 |
44 | postgres: 45 | 46 | ``` 47 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 48 | module.exports= { 49 | type: 'postgres', 50 | host: 'localhost', 51 | port: 5432, 52 | username: 'postgres', 53 | password: '123456', 54 | database: 'module_test', 55 | entities: ['src/**/**.entity.ts', 'node_modules/**/**.entity.js'], 56 | logger: 'advanced-console', 57 | logging: true, 58 | synchronize: true, 59 | dropSchema: false 60 | } 61 | ``` 62 |
63 | 64 |
65 | mysql: 66 | 67 | ``` 68 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 69 | module.exports= { 70 | type: 'mysql', 71 | host: 'localhost', 72 | port: 3306, 73 | username: 'test', 74 | password: 'test', 75 | database: 'module_test', 76 | entities: [ 77 | 'src/**/**.entity.ts' 78 | ] 79 | logging: true, 80 | synchronize: true 81 | } 82 | ``` 83 |
84 |
85 | sqlite: 86 | 87 | 88 | ``` 89 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 90 | module.exports= { 91 | type: 'sqlite', 92 | database: 'cms_test.db', 93 | storage: 'src/entities/*.entity.ts', 94 | synchronize: true, 95 | entities:[ 96 | 'src/entities/*.entity.ts' 97 | ] 98 | } 99 | ``` 100 |
101 | 102 | 103 | 104 | 配置完成,运行项目 105 | - `npm run start` 106 | 如果没有报错,打开浏览器访问: localhost:3000/graphql 107 | 108 | ## 实体定义 109 | - `classify`: 文章分类。一个文章分类对应多篇文章。 110 | - `article`: 文章。一篇文章对应一个文章分类。不同分类的文章需要输入的内容可能不同,详见信息项。 111 | - `item`: 信息项。你可以设置你想要文章展示的项,并将其与文章分类相绑定。这样在添加对应分类的文章时,也需要添加该信息项的值。 112 | - `page-sort`: 页面分类。等同于文章分类。 113 | - `page`: 页面。页面的作用是控制页面上需要显示的内容,如:页面footer、header、友情链接、联系我们等部分的内容。 114 | - `pictureGroup`: 轮播图组。可以设置多组轮播图,使其在不同的页面上展示。 115 | 116 | ## 常用接口介绍 117 | 118 | ### 文章分类 119 | 120 | **Mutation**: 121 | 122 | - `addClassify` 添加文章分类 123 | - `deleteClassify` 删除文章分类 124 | 125 | **Query**: 126 | 127 | -`getAllClassify` 获取全部分类结构 128 | -`getOneClassify` 获取单个文章分类数据 129 | 130 | ### 文章 131 | 132 | **Mutation**: 133 | 134 | - `createArticle` 创建文章 135 | - `recycleArticleByIds` 批量将文章放入回收站 136 | - `auditArticle` 批量审核文章 137 | 138 | **Query**: 139 | 140 | - `getAllArticle` 根据条件分页搜索文章 141 | - `getRecycleArticle` 根据条件搜索回收站文章 142 | - `getArticleById` 通过id获取文章详情 143 | 144 | ### 信息项 145 | 146 | **Mutation**: 147 | 148 | - `createItem` 创建信息项 149 | - `deleteItem` 删除信息项 150 | 151 | **Query**: 152 | 153 | - `getAllItem` 获取所有信息项 154 | - `getOneItem` 通过id获取信息项详情 155 | 156 | ### 轮播图 157 | 158 | **Query**: 159 | 160 | - `findpG` 通过id查看图片组信息 161 | 162 | **Mutation**: 163 | 164 | - `addPicGroup` 新增图片组 165 | - `addPicture` 新增图片组的图片 166 | 167 | tips:系统已自动创建一个根分类,所以用户在创建顶级分类时"上级分类"应传'root',即: 168 | ``` 169 | mutation{ 170 | addClassify(classify:{ 171 | label:"分类1", 172 | value:"classify_1", 173 | parent:{value:"root"} 174 | }) 175 | { 176 | code 177 | message 178 | } 179 | } 180 | ``` 181 | 182 | ## 项目结构 183 | 184 | ``` 185 | . 186 | ├── ormconfig.js 数据库配置 187 | ├── src 188 | │ ├── cms.module.ts cms模块配置 189 | │ ├── entities 实体对象 190 | │ ├── graphqls graphql接口 191 | │ ├── interceptors 拦截器 192 | │ ├── interfaces 接口定义 193 | │ ├── resolvers resolver层 194 | │ └── services 方法实现层 195 | └── starter 196 | ├── app.module.ts 根模块 197 | └── main.ts 程序入口文件 198 | ``` 199 | 200 | 201 | ## 环境安装 202 | 203 | install 204 | 205 | **安装Node.js** 206 | 207 |
208 | Windows 209 | 210 | 1. [点击下载 Node.js](https://npm.taobao.org/mirrors/node/v10.15.1/node-v10.15.1-x64.msi) 211 | 2. 安装Node.js 212 | 213 | Powershell/CMD 可以打印出这个说明安装成功。(部分系统需要重启后环境变量才生效) 214 | 215 | ``` 216 | >> node -v 217 | v10.15.1 218 | >> npm -v 219 | 6.4 220 | ``` 221 |
222 | 223 |
224 | Macos 225 | 226 | 1. [点击下载 Node.js](https://npm.taobao.org/mirrors/node/v10.15.1/node-v10.15.1.pkg) 227 | 2. 安装Node.js 228 | 229 | 打印出这个说明安装成功。(部分系统需要重启后环境变量才生效) 230 | ``` 231 | >> node -v 232 | v10.15.1 233 | >> npm -v 234 | 6.4 235 | ``` 236 |
237 | 238 | 239 |
240 | Ubuntu/Debian (支持ARM及X86平台) 241 | 242 | ``` 243 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 244 | sudo apt-get install -y nodejs 245 | ``` 246 | (如果安装缓慢,可以使用[国内镜像源](http://mirrors.ustc.edu.cn/help/nodesource.html)) 247 | 终端可以打出以下信息说明安装成功: 248 | ``` 249 | $ node -v 250 | v10.15.1 251 | $ npm -v 252 | 6.4 253 | ``` 254 |
255 | 256 |
257 | Centos/Redhat/Fedora (支持X86平台) 258 | 259 | ``` 260 | curl -sL https://rpm.nodesource.com/setup_10.x | bash - 261 | ``` 262 | (如果安装缓慢,可以使用[国内镜像源](http://mirrors.ustc.edu.cn/help/nodesource.html)) 263 | 终端可以打出以下信息说明安装成功: 264 | ``` 265 | $ node -v 266 | v10.15.1 267 | $ npm -v 268 | 6.4 269 | ``` 270 |
271 | 272 |
273 | 使用 NVM 安装(支持 所有 Linux 及 Raspbian ,支持多版本管理) 274 | 275 | ``` 276 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash 277 | 278 | ``` 279 | 如果没 curl ,可以使用 wget 安装 280 | ``` 281 | wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash 282 | ``` 283 | 使用 NVM 安装nodejs : 284 | ``` 285 | nvm install --lts 286 | ``` 287 | 终端可以打出以下信息说明安装成功: 288 | ``` 289 | $ node -v 290 | v10.15.1 291 | $ npm -v 292 | 6.4 293 | ``` 294 |
295 | 296 |
297 | 使用 snap 安装(支持 所有 Linux ) 298 | 299 | ``` 300 | sudo snap install node --classic --channel=10 301 | 302 | ``` 303 | (如果提示 snap 不存在,请先安装 snapd) 304 | 终端可以打出以下信息说明安装成功: 305 | ``` 306 | $ node -v 307 | v10.15.1 308 | $ npm -v 309 | 6.4 310 | ``` 311 |
312 | 313 | 314 | 315 | 316 | **安装数据库** 317 | 318 |
319 | Postgresql (推荐) 320 | 321 | Windows 和 Mac 用户 [点击下载安装包](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads) 322 | 323 | Linux 用户使用 apt/yum 等直接安装: 324 | 325 | ``` 326 | apt install postgresql 327 | ``` 328 | 或者使用 snap : 329 | 330 | ``` 331 | snap install postgresql10 332 | ``` 333 | 如果要开外部访问,以及其他配置,请参考 [postgresql配置]() 334 |
335 | Sqlite3 无需安装,Mysql 及 其他数据库 请参考官方文档自行安装。 336 | 337 | 338 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - 开始 2 | - [必要环境](environment) 3 | - [安装环境](install) 4 | - [下载项目](gitclone) 5 | - [配置文件](config) 6 | - [启动项目](start) 7 | - [大吉大利](jinwanchiji) 8 | 9 | - [向导](guide) 10 | 11 | - [详解](techniques) 12 | 13 | - [FAQ](faq) 14 | 15 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | 创建项目需要的数据库并在`ormconfig.js`文件中配置数据库连接; 2 | 3 |
4 | postgres: 5 | 6 | ``` 7 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 8 | module.exports= { 9 | type: 'postgres', 10 | host: 'localhost', 11 | port: 5432, 12 | username: 'postgres', 13 | password: '123456', 14 | database: 'module_test', 15 | entities: ['src/**/**.entity.ts', 'node_modules/**/**.entity.js'], 16 | logger: 'advanced-console', 17 | logging: true, 18 | synchronize: true, 19 | dropSchema: false 20 | } 21 | ``` 22 |
23 | 24 |
25 | mysql: 26 | 27 | ``` 28 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 29 | module.exports= { 30 | type: 'mysql', 31 | host: 'localhost', 32 | port: 3306, 33 | username: 'test', 34 | password: 'test', 35 | database: 'module_test', 36 | entities: [ 37 | 'src/**/**.entity.ts' 38 | ] 39 | logging: true, 40 | synchronize: true 41 | } 42 | ``` 43 |
44 |
45 | sqlite: 46 | 47 | 48 | ``` 49 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 50 | module.exports= { 51 | type: 'sqlite', 52 | database: 'cms_test.db', 53 | storage: 'src/entities/*.entity.ts', 54 | synchronize: true, 55 | entities:[ 56 | 'src/entities/*.entity.ts' 57 | ] 58 | } 59 | ``` 60 |
61 | 62 | -------------------------------------------------------------------------------- /docs/environment.md: -------------------------------------------------------------------------------- 1 | ## 环境要求: 2 | 3 | Nodejs: 8+ 4 | 5 | 数据库: PostgreSQL 9.5+, MariaDB 10.2+, Mysql 5.7+, SQLite, Mongodb, MS SQL Server, Oracle (任意一种) 6 | 7 | [1分钟安装环境](#install) -------------------------------------------------------------------------------- /docs/gitclone.md: -------------------------------------------------------------------------------- 1 | gitclone源码; 2 | - `git clone https://github.com/notadd/nt-cms.git` 3 | 4 | 进入项目目录下安装项目依赖 5 | 6 | - `npm install` 7 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | ## 结构设计 2 | 3 | ### 用户模块 4 | `user`: 用户实体。一个用户可以对应多个`role`,即角色。 5 | `role` 用户角色。一个角色可以对应多个`user`。 6 | 7 | ### cms模块 8 | `classify`: 文章分类实体。每个分类可以有一个上级分类及多个下级分类。一个文章分类下可以有多篇`article`,即文章。分类除了基本属性外,还可以使用信息项功能生成你所需要的特殊属性。 9 | `article`: 文章实体。每个文章必须属于一个分类。 10 | `page-sort`: 页面分类,同文章分类。 11 | `page`: 页面实体。用来控制页面上展现的内容,如'友情链接'、'关于我们'、'公司介绍'等。 12 | 13 | ### 插件 14 | `picture-group`: 轮播图组实体。一个轮播图组中有多张图片。可以设置多个轮播图组灵活调用。 15 | `picture`: 图片实体。 16 | 17 | ## API接口介绍 18 | 19 | ### 文章分类 20 | 21 | **Mutation**: 22 | 23 | - `addClassify` 添加文章分类 24 | - `deleteClassify` 删除文章分类 25 | 26 | **Query**: 27 | 28 | -`getAllClassify` 获取全部分类结构 29 | -`getOneClassify` 获取单个文章分类数据 30 | 31 | ### 文章 32 | 33 | **Mutation**: 34 | 35 | - `createArticle` 创建文章 36 | - `recycleArticleByIds` 批量将文章放入回收站 37 | - `auditArticle` 批量审核文章 38 | 39 | **Query**: 40 | 41 | - `getAllArticle` 根据条件分页搜索文章 42 | - `getRecycleArticle` 根据条件搜索回收站文章 43 | - `getArticleById` 通过id获取文章详情 44 | 45 | ### 轮播图 46 | 47 | **Query**: 48 | 49 | - `findpG` 通过id查看图片组信息 50 | - `findPicture` 通过id查看图片具体信息 51 | 52 | **Mutation**: 53 | 54 | - `addPicGroup` 新增图片组 55 | - `addPicture` 新增图片组的图片 56 | 57 | tips:系统已自动创建一个根分类,所以用户在创建顶级分类时"上级分类"应传'root',即: 58 | ``` 59 | mutation{ 60 | addClassify(classify:{ 61 | label:"分类1", 62 | value:"classify_1", 63 | parent:{value:"root"} 64 | }) 65 | { 66 | code 67 | message 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | ## 环境安装 2 | 3 | install 4 | 5 | **安装Node.js** 6 | 7 |
8 | Windows 9 | 10 | 1. [点击下载 Node.js](https://npm.taobao.org/mirrors/node/v10.15.1/node-v10.15.1-x64.msi) 11 | 2. 安装Node.js 12 | 13 | Powershell/CMD 可以打印出这个说明安装成功。(部分系统需要重启后环境变量才生效) 14 | 15 | ``` 16 | >> node -v 17 | v10.15.1 18 | >> npm -v 19 | 6.4 20 | ``` 21 |
22 | 23 |
24 | Macos 25 | 26 | 1. [点击下载 Node.js](https://npm.taobao.org/mirrors/node/v10.15.1/node-v10.15.1.pkg) 27 | 2. 安装Node.js 28 | 29 | 打印出这个说明安装成功。(部分系统需要重启后环境变量才生效) 30 | ``` 31 | >> node -v 32 | v10.15.1 33 | >> npm -v 34 | 6.4 35 | ``` 36 |
37 | 38 | 39 |
40 | Ubuntu/Debian (支持ARM及X86平台) 41 | 42 | ``` 43 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - 44 | sudo apt-get install -y nodejs 45 | ``` 46 | (如果安装缓慢,可以使用[国内镜像源](http://mirrors.ustc.edu.cn/help/nodesource.html)) 47 | 终端可以打出以下信息说明安装成功: 48 | ``` 49 | $ node -v 50 | v10.15.1 51 | $ npm -v 52 | 6.4 53 | ``` 54 |
55 | 56 |
57 | Centos/Redhat/Fedora (支持X86平台) 58 | 59 | ``` 60 | curl -sL https://rpm.nodesource.com/setup_10.x | bash - 61 | ``` 62 | (如果安装缓慢,可以使用[国内镜像源](http://mirrors.ustc.edu.cn/help/nodesource.html)) 63 | 终端可以打出以下信息说明安装成功: 64 | ``` 65 | $ node -v 66 | v10.15.1 67 | $ npm -v 68 | 6.4 69 | ``` 70 |
71 | 72 |
73 | 使用 NVM 安装(支持 所有 Linux 及 Raspbian ,支持多版本管理) 74 | 75 | ``` 76 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash 77 | 78 | ``` 79 | 如果没 curl ,可以使用 wget 安装 80 | ``` 81 | wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash 82 | ``` 83 | 使用 NVM 安装nodejs : 84 | ``` 85 | nvm install --lts 86 | ``` 87 | 终端可以打出以下信息说明安装成功: 88 | ``` 89 | $ node -v 90 | v10.15.1 91 | $ npm -v 92 | 6.4 93 | ``` 94 |
95 | 96 |
97 | 使用 snap 安装(支持 所有 Linux ) 98 | 99 | ``` 100 | sudo snap install node --classic --channel=10 101 | 102 | ``` 103 | (如果提示 snap 不存在,请先安装 snapd) 104 | 终端可以打出以下信息说明安装成功: 105 | ``` 106 | $ node -v 107 | v10.15.1 108 | $ npm -v 109 | 6.4 110 | ``` 111 |
112 | 113 | 114 | 115 | 116 | **安装数据库** 117 | 118 |
119 | Postgresql (推荐) 120 | 121 | Windows 和 Mac 用户 [点击下载安装包](https://www.enterprisedb.com/downloads/postgres-postgresql-downloads) 122 | 123 | Linux 用户使用 apt/yum 等直接安装: 124 | 125 | ``` 126 | apt install postgresql 127 | ``` 128 | 或者使用 snap : 129 | 130 | ``` 131 | snap install postgresql10 132 | ``` 133 | 如果要开外部访问,以及其他配置,请参考 [postgresql配置]() 134 |
135 | Sqlite3 无需安装,Mysql 及 其他数据库 请参考官方文档自行安装。 136 | 137 | 138 | -------------------------------------------------------------------------------- /docs/start.md: -------------------------------------------------------------------------------- 1 | ## 项目启动 2 | 3 | 配置完成,运行项目 4 | - `npm run start` 5 | 如果没有报错,打开浏览器访问: localhost:3000/graphql 6 | -------------------------------------------------------------------------------- /docs/techniques.md: -------------------------------------------------------------------------------- 1 | ## 项目结构 2 | 3 | ``` 4 | . 5 | ├── ormconfig.js 数据库配置 6 | ├── src 7 | │ ├── cms 8 | │ │ ├── cms.module.ts cms模块配置 9 | │ │ ├── entities 实体对象 10 | │ │ ├── graphqls graphql接口 11 | │ │ ├── interfaces 接口定义 12 | │ │ ├── resolvers resolver层 13 | │ │ └── services 方法实现层 14 | │ ├── interceptors 拦截器 15 | │ └── user 16 | │ ├── auth 用户验证模块 17 | │ ├── user.module.ts 用户模块配置 18 | │ └── utils 工具类 19 | └── starter 20 | ├── app.module.ts 根模块 21 | └── main.ts 程序入口文件 22 | ``` 23 | 24 | ## 分类信息项 25 | 26 | 什么是信息项? 27 | 我们可以看到,文章中只有标题、摘要、内容等基本属性,如果你想要给某一分类下的文章添加他自己独有的属性,那么就可以使用信息项来完成。 28 | 信息项通过`JSON Schema Form`实现,利用JSON自动生成表单。只需在后台编辑分类页面输入相应的JSON,即可在文章页面出现需要填写的项。后期会更新为可视化图形界面,只需拖动相应的表单图形至选项中即可。 -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "0.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /ormconfig.js: -------------------------------------------------------------------------------- 1 | const SOURCE_PATH = process.env.NODE_ENV === 'development' ? 'packages' : 'src'; 2 | module.exports= { 3 | type: 'postgres', 4 | host: 'localhost', 5 | port: 5432, 6 | username: 'postgres', 7 | password: '123456', 8 | database: 'module_test', 9 | entities: ['src/**/**.entity.ts', 'node_modules/**/**.entity.js'], 10 | logger: 'advanced-console', 11 | logging: true, 12 | synchronize: true, 13 | dropSchema: false 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nt_cms", 3 | "version": "1.0.0", 4 | "description": "nt_cms", 5 | "author": "yk", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "node --max_old_space_size=4096 ./node_modules/gulp/bin/gulp.js build", 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "start": "ts-node -r tsconfig-paths/register starter/main.ts", 11 | "start:dev": "nodemon", 12 | "start:debug": "nodemon --config nodemon-debug.json", 13 | "start:prod": "node dist/main.js", 14 | "lint": "tslint -p tsconfig.json -c tslint.json", 15 | "snyk-protect": "snyk protect", 16 | "prepare": "npm run snyk-protect" 17 | }, 18 | "dependencies": { 19 | "@nestjs/common": "5.7.4", 20 | "@nestjs/core": "5.7.4", 21 | "@nestjs/graphql": "6.2.1", 22 | "@nestjs/typeorm": "5.3.0", 23 | "apollo-server-express": "2.5.0", 24 | "@notadd/addon-sms": "3.0.0", 25 | "graphql": "14.3.0", 26 | "graphql-tools": "4.0.4", 27 | "graphql-type-json": "0.3.0", 28 | "moment": "2.24.0", 29 | "pg": "7.11.0", 30 | "reflect-metadata": "0.1.13", 31 | "rimraf": "2.6.3", 32 | "rxjs": "6.4.0", 33 | "ts-node": "8.1.0", 34 | "typeorm": "0.2.17", 35 | "typescript": "3.2.2", 36 | "underscore": "1.9.1", 37 | "snyk": "^1.214.0" 38 | }, 39 | "devDependencies": { 40 | "@types/bcryptjs": "2.4.2", 41 | "@types/graphql": "14.2.0", 42 | "@types/graphql-type-json": "0.3.0", 43 | "@types/i18n": "0.8.5", 44 | "@types/jsonwebtoken": "8.3.2", 45 | "@types/node": "11.13.11", 46 | "gulp": "4.0.2", 47 | "gulp-sequence": "1.0.0", 48 | "gulp-sourcemaps": "2.6.5", 49 | "gulp-tslint": "8.1.4", 50 | "gulp-typescript": "5.0.1", 51 | "lerna": "3.14.1", 52 | "nodemon": "1.19.0", 53 | "ts-node": "8.1.0", 54 | "tsconfig-paths": "3.8.0", 55 | "tslint": "5.16.0" 56 | }, 57 | "snyk": true 58 | } 59 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/cms/cms.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnModuleInit } from '@nestjs/common'; 2 | import { TypeOrmModule, InjectRepository } from '@nestjs/typeorm'; 3 | import { APP_INTERCEPTOR } from '@nestjs/core'; 4 | import { TreeRepository } from 'typeorm'; 5 | import { ErrorsInterceptor } from '../interceptors/errors.interceptor'; 6 | import { Article } from './entities/article.entity'; 7 | import { Classify } from './entities/classify.entity'; 8 | import { ArticleResolver } from './resolvers/article.resolver'; 9 | import { ArticleService } from './services/article.service'; 10 | import { ClassifyResolver } from './resolvers/classify.resolver'; 11 | import { ClassifyService } from './services/classify.service'; 12 | import { picGroupResolver } from './resolvers/pic-group.resolver'; 13 | import { PicGroupService } from './services/pic-group.service'; 14 | import { PictureGroup } from './entities/picture-group.entity'; 15 | import { Picture } from './entities/picture.entity'; 16 | import { Page } from './entities/page.entity'; 17 | import { PageResolver } from './resolvers/page.resolver'; 18 | import { PageService } from './services/page.service'; 19 | import { PageSort } from './entities/page-sort.entity'; 20 | import { PageSortResolver } from './resolvers/page-sort.resolver'; 21 | import { PageSortService } from './services/page-sort.service'; 22 | import { Content } from './entities/content.entity'; 23 | import { Index } from './entities/index.entity'; 24 | import { IndexResolver } from './resolvers/index.resolver'; 25 | import { IndexService } from './services/index.service'; 26 | 27 | @Module({ 28 | imports: [ 29 | TypeOrmModule.forFeature([ 30 | Classify, Article, PictureGroup, Picture, Page, PageSort, Content, Index 31 | ]), 32 | ], 33 | providers: [ 34 | { provide: APP_INTERCEPTOR, useClass: ErrorsInterceptor }, 35 | ArticleResolver, ArticleService, 36 | ClassifyResolver, ClassifyService, 37 | picGroupResolver, PicGroupService, 38 | PageResolver, PageService, 39 | PageSortResolver, PageSortService, 40 | IndexResolver, IndexService 41 | ] 42 | }) 43 | export class CmsModule implements OnModuleInit { 44 | constructor( 45 | @InjectRepository(Classify) private readonly claRepo: TreeRepository, 46 | @InjectRepository(PageSort) private readonly psRepo: TreeRepository, 47 | @InjectRepository(Index) private readonly indexRepo: TreeRepository, 48 | private readonly classifyService: ClassifyService, 49 | private readonly pageSortService: PageSortService, 50 | ) { } 51 | 52 | async onModuleInit() { 53 | await this.createRootClassify(); 54 | await this.createPageSortClassify(); 55 | await this.createIndex(); 56 | } 57 | 58 | private async createRootClassify() { 59 | const root = await this.claRepo.findOne({ where: { value: 'root' } }); 60 | if (!root) { 61 | await this.classifyService.addClassify({ label: '总分类', value: 'root', parent: { value: '' }, onlyChildrenArt: true, order: 1, structure: '' }); 62 | } 63 | } 64 | 65 | private async createPageSortClassify() { 66 | const root = await this.psRepo.findOne({ where: { value: 'root' } }); 67 | if (!root) { 68 | await this.pageSortService.createPageSort({ label: '总分类', value: 'root', parent: { value: '' }, structure: '' }); 69 | } 70 | } 71 | 72 | private async createIndex() { 73 | const root = await this.indexRepo.findOne(); 74 | if (!root) { 75 | await this.indexRepo.save(await this.indexRepo.create({ name: '', filing: '', link: '', code: '' })); 76 | } 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/cms/entities/article.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, JoinColumn, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; 2 | import * as moment from 'moment'; 3 | import { Classify } from './classify.entity'; 4 | @Entity('article') 5 | export class Article { 6 | /*文章Id*/ 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | /*文章作者*/ 11 | @Column({ 12 | nullable: true, 13 | length: 120, 14 | }) 15 | username: string; 16 | 17 | /*文章发布人id*/ 18 | @Column({ 19 | nullable: true 20 | }) 21 | owner: number; 22 | 23 | /*文章标题*/ 24 | @Column({ 25 | nullable: true, 26 | length: 120, 27 | }) 28 | title: string; 29 | 30 | /*关键词*/ 31 | @Column({ 32 | nullable: true 33 | }) 34 | keywords: string; 35 | 36 | @Column({ 37 | default: 0 38 | }) 39 | like: number; 40 | 41 | /* 访问量*/ 42 | @Column({ 43 | default: 0 44 | }) 45 | views: number; 46 | 47 | /*封面图片地址*/ 48 | @Column({ 49 | nullable: true, 50 | length: 500, 51 | }) 52 | cover: string; 53 | 54 | 55 | /*摘要*/ 56 | @Column({ 57 | nullable: true, 58 | length: 500, 59 | }) 60 | abstract: string; 61 | 62 | /*内容*/ 63 | @Column({ 64 | nullable: true, 65 | type: 'text', 66 | }) 67 | content: string; 68 | 69 | /*置顶,0: 不置顶,1:分类置顶,2:全局置顶*/ 70 | @Column({ 71 | default: 0 72 | }) 73 | top: number; 74 | 75 | /*来源*/ 76 | @Column({ 77 | nullable: true, 78 | length: 120, 79 | }) 80 | source: string; 81 | 82 | /*文章状态, 0 待审核 1 审核通过 2 被拒绝 */ 83 | @Column({ 84 | default: 0 85 | }) 86 | status: number; 87 | 88 | /*拒绝原因*/ 89 | @Column({ 90 | type: 'text', 91 | nullable: true 92 | }) 93 | refuseReason: string; 94 | 95 | /*来源链接*/ 96 | @Column({ 97 | nullable: true, 98 | length: 200, 99 | }) 100 | sourceUrl: string; 101 | 102 | /*删除(回收站)*/ 103 | @Column({ 104 | nullable: true, 105 | default: false 106 | }) 107 | recycling: boolean; 108 | 109 | /* 是否隐藏文章*/ 110 | @Column({ 111 | default: false 112 | }) 113 | hidden: boolean; 114 | 115 | @Column({ 116 | nullable: true 117 | }) 118 | structure: string; 119 | 120 | /*发布时间*/ 121 | @Column({ 122 | nullable: true, 123 | transformer: { 124 | from: (date) => { 125 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 126 | }, 127 | to: (date) => { 128 | date = date ? date : new Date(); 129 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 130 | } 131 | } 132 | }) 133 | createdAt: string; 134 | 135 | /*修改时间*/ 136 | @Column({ 137 | nullable: true, 138 | transformer: { 139 | from: (date) => { 140 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 141 | }, 142 | to: (date) => { 143 | date = date ? date : new Date(); 144 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 145 | } 146 | } 147 | }) 148 | modifyAt: string; 149 | 150 | @Column({ 151 | nullable: true, 152 | type: 'varchar', 153 | transformer: { 154 | from: (data) => { 155 | if (data) { 156 | return JSON.parse(data); 157 | } else { 158 | // tslint:disable-next-line:no-null-keyword 159 | return null; 160 | } 161 | }, 162 | to: (data) => { 163 | data = data ? JSON.stringify(data) : undefined; 164 | return data; 165 | } 166 | } 167 | }) 168 | artInfos: JSON; 169 | 170 | /*分类Id*/ 171 | @ManyToOne(type => Classify, classify => classify.articles, { onDelete: 'CASCADE', cascade: true }) 172 | @JoinColumn({ 173 | name: 'classifyId', 174 | referencedColumnName: 'id' 175 | }) 176 | classify: Classify; 177 | 178 | } 179 | 180 | -------------------------------------------------------------------------------- /src/cms/entities/classify.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrimaryGeneratedColumn, 3 | Column, 4 | Entity, 5 | OneToMany, Tree, TreeChildren, TreeParent, ManyToOne 6 | } from 'typeorm'; 7 | import { Article } from './article.entity'; 8 | 9 | @Entity('classify') 10 | @Tree('nested-set') 11 | export class Classify { 12 | /*分类Id*/ 13 | @PrimaryGeneratedColumn() 14 | id: number; 15 | 16 | /*分类名称*/ 17 | @Column({ 18 | nullable: true, 19 | length: 120, 20 | }) 21 | label: string; 22 | 23 | @Column({ 24 | comment: '分类别名', 25 | nullable: true 26 | }) 27 | value: string; 28 | 29 | @Column({ 30 | nullable: true 31 | }) 32 | order: number; 33 | 34 | @Column({ 35 | comment: '只显示子级分类文章', 36 | default: false 37 | }) 38 | onlyChildrenArt: boolean; 39 | 40 | @Column({ 41 | nullable: true 42 | }) 43 | structure: string; 44 | 45 | @TreeChildren() 46 | children: Classify[]; 47 | 48 | @TreeParent() 49 | parent: Classify; 50 | 51 | 52 | @OneToMany(type => Article, article => article.classify) 53 | articles: Article[]; 54 | 55 | // 信息项JSON表单 56 | @Column({ 57 | nullable: true, 58 | type: 'varchar', 59 | transformer: { 60 | from: (data) => { 61 | if (data) { 62 | return JSON.parse(data); 63 | } else { 64 | // tslint:disable-next-line:no-null-keyword 65 | return null; 66 | } 67 | }, 68 | to: (data) => { 69 | data = data ? JSON.stringify(data) : undefined; 70 | return data; 71 | } 72 | } 73 | }) 74 | itemJson: JSON; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/cms/entities/content.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; 2 | import { Page } from './page.entity'; 3 | 4 | @Entity('content') 5 | export class Content { 6 | @PrimaryGeneratedColumn({ 7 | comment: '自增id' 8 | }) 9 | id: number; 10 | 11 | @Column({ 12 | comment: '显示名称', 13 | nullable: true 14 | }) 15 | name: string; 16 | 17 | @Column({ 18 | comment: '别名', 19 | unique: true 20 | }) 21 | alias: string; 22 | 23 | @Column({ 24 | comment: '页面内容', 25 | type: 'text', 26 | nullable: true 27 | }) 28 | value: string; 29 | 30 | @ManyToOne(type => Page, page => page.contents, { onDelete: 'CASCADE', cascade: true }) 31 | page: Page; 32 | 33 | } -------------------------------------------------------------------------------- /src/cms/entities/index.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, JoinColumn, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; 2 | import * as moment from 'moment'; 3 | 4 | @Entity('index') 5 | export class Index { 6 | /*Id*/ 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({ 11 | nullable: true 12 | }) 13 | name: string; 14 | 15 | @Column({ 16 | nullable: true 17 | }) 18 | filing: string; 19 | 20 | @Column({ 21 | nullable: true 22 | }) 23 | link: string; 24 | 25 | @Column({ 26 | default: false 27 | }) 28 | isCheckArticle: boolean; 29 | 30 | @Column({ 31 | nullable: true, 32 | type: 'text' 33 | }) 34 | code: string; 35 | 36 | } 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/cms/entities/page-sort.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrimaryGeneratedColumn, 3 | Column, 4 | Entity, 5 | OneToMany, Tree, TreeChildren, TreeParent 6 | } from 'typeorm'; 7 | import { Page } from './page.entity'; 8 | 9 | @Entity('page_sort') 10 | @Tree('nested-set') 11 | export class PageSort { 12 | /*分类Id*/ 13 | @PrimaryGeneratedColumn() 14 | id: number; 15 | 16 | /*分类名称*/ 17 | @Column({ 18 | nullable: true, 19 | length: 120, 20 | }) 21 | label: string; 22 | 23 | @Column({ 24 | comment: '分类别名', 25 | nullable: true, 26 | unique: true 27 | }) 28 | value: string; 29 | 30 | @Column({ 31 | nullable: true 32 | }) 33 | structure: string; 34 | 35 | @TreeChildren() 36 | children: PageSort[]; 37 | 38 | @TreeParent() 39 | parent: PageSort; 40 | 41 | @OneToMany(type => Page, page => page.pageSort) 42 | pages: Page[]; 43 | } 44 | -------------------------------------------------------------------------------- /src/cms/entities/page.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; 2 | import { Content } from './content.entity'; 3 | import { PageSort } from './page-sort.entity'; 4 | 5 | @Entity('page') 6 | export class Page { 7 | @PrimaryGeneratedColumn({ 8 | comment: '自增id' 9 | }) 10 | id: number; 11 | 12 | @Column({ 13 | comment: '页面名称' 14 | }) 15 | name: string; 16 | 17 | @Column({ 18 | comment: '页面别名' 19 | }) 20 | alias: string; 21 | 22 | @Column({ 23 | comment: '最后修改时间', 24 | nullable: true 25 | }) 26 | lastUpdateTime: string; 27 | 28 | @Column({ 29 | nullable: true 30 | }) 31 | structure: string; 32 | 33 | @OneToMany(type => Content, content => content.page, { cascade: ['insert', 'update'] }) 34 | contents: Content[]; 35 | 36 | @ManyToOne(type => PageSort, pageSort => pageSort.pages) 37 | pageSort: PageSort; 38 | 39 | } -------------------------------------------------------------------------------- /src/cms/entities/picture-group.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; 2 | import { Picture } from './picture.entity'; 3 | 4 | // 图组实体 5 | @Entity('picture_group') 6 | export class PictureGroup { 7 | @PrimaryGeneratedColumn({ comment: '自增主键', }) 8 | id: number; 9 | 10 | @Column({ comment: '图组名称', length: 20 }) // , unique : true }) 11 | name: string; 12 | 13 | @OneToMany(type => Picture, picture => picture.pictureGroup) 14 | pictures: Picture[]; 15 | } -------------------------------------------------------------------------------- /src/cms/entities/picture.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne } from 'typeorm'; 2 | import { PictureGroup } from './picture-group.entity'; 3 | 4 | // 图片实体 5 | @Entity('picture') 6 | export class Picture { 7 | @PrimaryGeneratedColumn({ comment: '自增主键', }) 8 | id: number; 9 | 10 | @Column({ comment: '图片地址'}) 11 | name: string; 12 | 13 | @Column({ comment: '图片跳转链接', nullable: true }) 14 | Url: string; 15 | 16 | // 图片标题文字将作为图片Alt形式显示 17 | @Column({ comment: '图片名称,', nullable: true }) 18 | title: string; 19 | 20 | // 展示顺序 21 | @Column({ comment: '排序', nullable: true }) 22 | sequence: number; 23 | 24 | @ManyToOne(type => PictureGroup, pictureGroup => pictureGroup.pictures, { onDelete: 'CASCADE' }) 25 | pictureGroup: PictureGroup; 26 | } -------------------------------------------------------------------------------- /src/cms/entities/user-message.entity.ts: -------------------------------------------------------------------------------- 1 | import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; 2 | import * as moment from 'moment'; 3 | 4 | @Entity('user_message') 5 | export class UserMessage { 6 | 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | /*内容*/ 11 | @Column({ 12 | name: 'content', 13 | type: 'text' 14 | }) 15 | content: string; 16 | 17 | /*发布时间*/ 18 | @Column({ 19 | transformer: { 20 | from: (date) => { 21 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 22 | }, 23 | to: (date) => { 24 | date = date ? date : new Date(); 25 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 26 | } 27 | } 28 | }) 29 | createdAt: string; 30 | 31 | /* 消息所属人 */ 32 | @Column({ 33 | nullable: true 34 | }) 35 | owner: number; 36 | 37 | /* 是否已读 */ 38 | @Column({ 39 | default: false 40 | }) 41 | state: boolean; 42 | 43 | } -------------------------------------------------------------------------------- /src/cms/graphqls/article.types.graphql: -------------------------------------------------------------------------------- 1 | type Query{ 2 | getAllArticle(classifyAlias: String, createdAt: String, title: String, pageNumber: Int, pageSize: Int, endTime: String, username: String,top: Boolean): AllArticleResponse 3 | getRecycleArticle(classifyAlias: String, createdAt: String, title: String, pageNumber: Int, pageSize: Int, endTime: String, username: String,top: Boolean): AllArticleResponse 4 | getCheckArticle(classifyAlias: String, createdAt: String, title: String, pageNumber: Int, pageSize: Int, endTime: String, username: String,top: Boolean): AllArticleResponse 5 | getArticleById(id: Int): OneArticleResponse 6 | userGetArticles(alias: String, pageNumber: Int, pageSize: Int): UserGetArtResponse 7 | } 8 | 9 | type Mutation{ 10 | createArticle(article: CreateArticleInput): CommonResult 11 | updateArticle(article: UpdateArticleInput): CommonResult 12 | recycleArticleByIds(ids: [Int]): CommonResult 13 | deleteArticleByIds(ids: [Int]): CommonResult 14 | recoverArticleByIds(ids: [Int]): CommonResult 15 | auditArticle(ids: [Int], op: Int, refuseReason: String): CommonResult 16 | } 17 | 18 | type AllArticleResponse { 19 | code: Int 20 | message: String 21 | total: Int 22 | data: [Art] 23 | } 24 | 25 | type UserGetArtResponse { 26 | code: Int 27 | message: String 28 | total: Int 29 | data: [UserArt] 30 | } 31 | 32 | type UserArt { 33 | id: Int 34 | title: String 35 | content: String 36 | abstract: String 37 | classify: ClassifyData 38 | createdAt: String 39 | modifyAt: String 40 | cover: String 41 | keywords: String 42 | like: Int 43 | sourceUrl: String 44 | source: String 45 | } 46 | 47 | type Art { 48 | id: Int 49 | title: String 50 | source: String 51 | classify: OneClassifyData 52 | sourceUrl: String 53 | top: Int 54 | views: Int 55 | cover: String 56 | abstract: String 57 | content: String 58 | status: Int 59 | refuseReason: String 60 | recycling: Boolean 61 | hidden: Boolean 62 | createdAt: String 63 | modifyAt: String 64 | username: String 65 | artInfos: JSON 66 | keywords: String 67 | like: Int 68 | structure: String 69 | } 70 | 71 | type OneClassifyData { 72 | id: Int 73 | label: String 74 | value: String 75 | order: Int 76 | onlyChildrenArt: Boolean 77 | } 78 | 79 | type OneArticleResponse { 80 | code: Int 81 | message: String 82 | data: OneArt 83 | } 84 | 85 | type OneArt { 86 | pre: Int 87 | current: Art 88 | next: Int 89 | } 90 | 91 | input CreateArticleInput { 92 | title: String 93 | username: String 94 | classify: CreateArticleClassify 95 | cover: String 96 | abstract: String 97 | content: String 98 | top: Int 99 | hidden: Boolean 100 | source: String 101 | sourceUrl: String 102 | structure: String 103 | artInfos: JSON 104 | } 105 | 106 | input CreateArticleClassify { 107 | value: String 108 | } 109 | 110 | input UpdateArticleInput { 111 | id: Int 112 | title: String 113 | username: String 114 | classify: CreateArticleClassify 115 | cover: String 116 | abstract: String 117 | content: String 118 | top: Int 119 | hidden: Boolean 120 | source: String 121 | sourceUrl: String 122 | structure: String 123 | artInfos: JSON 124 | } 125 | -------------------------------------------------------------------------------- /src/cms/graphqls/classify.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getAllClassify(id: Int): AllClassifyResult 3 | getOneClassify(value: String): OneClassifyResult 4 | getParentClassify(id: Int): AllClassifyResult 5 | } 6 | 7 | type Mutation { 8 | addClassify(classify: AddClassifyInput): CommonResult 9 | deleteClassify(id: Int): CommonResult 10 | updateClassify(classify: UpdateClassifyInput): CommonResult 11 | mobileArticles(oldId: Int,newId: Int): CommonResult 12 | } 13 | 14 | type AllClassifyResult { 15 | code: Int 16 | message: String 17 | data: JSON 18 | } 19 | 20 | type OneClassifyResult { 21 | code: Int 22 | message: String 23 | data: JSON 24 | } 25 | 26 | type ClassifyData { 27 | id: Int 28 | label: String 29 | value: String 30 | onlyChildrenArt: Boolean 31 | itemJson: JSON 32 | } 33 | 34 | type CmsInfoItemData { 35 | id: Int 36 | name: String 37 | explain: String 38 | style: String 39 | regular: String 40 | info: String 41 | } 42 | 43 | type CommonResult { 44 | code: Int 45 | message: String 46 | } 47 | 48 | input AddClassifyInput { 49 | label: String 50 | value: String 51 | parent: ParentClassify 52 | onlyChildrenArt: Boolean 53 | structure: String 54 | itemJson: JSON 55 | } 56 | 57 | input ParentClassify { 58 | value: String 59 | } 60 | 61 | input UpdateClassifyInput { 62 | id: Int 63 | label: String 64 | value: String 65 | onlyChildrenArt: Boolean 66 | parent: ParentClassify 67 | structure: String 68 | itemJson: JSON 69 | } 70 | -------------------------------------------------------------------------------- /src/cms/graphqls/index.types.graphql: -------------------------------------------------------------------------------- 1 | type Query{ 2 | getIndex: IndexResult 3 | } 4 | 5 | type Mutation{ 6 | updateIndex(index: UpdateIndexInput): CommonResult 7 | } 8 | 9 | type IndexResult { 10 | code: Int 11 | message: String 12 | data: Index 13 | } 14 | 15 | type Index { 16 | name: String 17 | filing: String 18 | link: String 19 | isCheckArticle: Boolean 20 | code: String 21 | } 22 | 23 | input UpdateIndexInput { 24 | name: String 25 | filing: String 26 | link: String 27 | isCheckArticle: Boolean 28 | code: String 29 | } -------------------------------------------------------------------------------- /src/cms/graphqls/page-sort.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getAllPageSort: AllPageSortResult 3 | getOnePageSort(alias: String): OnePageSortResult 4 | } 5 | 6 | type Mutation { 7 | createPageSort(pageSort: CreatePageSortInput): CommonResult 8 | updatePageSort(pageSort: UpdatePageSortInput): CommonResult 9 | deletePageSort(id: Int): CommonResult 10 | } 11 | 12 | input CreatePageSortInput { 13 | label: String 14 | value: String 15 | parent: ParentPageSort 16 | structure: String 17 | } 18 | 19 | input ParentPageSort { 20 | value: String 21 | } 22 | 23 | input UpdatePageSortInput { 24 | id: Int 25 | label: String 26 | value: String 27 | parent: ParentPageSort 28 | structure: String 29 | } 30 | 31 | type AllPageSortResult { 32 | code: Int 33 | message: String 34 | data: JSON 35 | } 36 | 37 | type PageSort { 38 | id: Int 39 | label: String 40 | value: String 41 | parent: PageSort 42 | children: [PageSort] 43 | structure: String 44 | } 45 | 46 | type OnePageSortResult { 47 | code: Int 48 | message: String 49 | data: PageSort 50 | } -------------------------------------------------------------------------------- /src/cms/graphqls/page.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getAllPage(name: String, alias: String, pageNumber: Int!, pageSize: Int!): AllPageResult 3 | getOnePage(alias: String): OnePageResult 4 | } 5 | 6 | type Mutation { 7 | createPage(page: CreatePageRequest): CommonResult 8 | updatePage(page: UpdatePageRequest): CommonResult 9 | deletePage(alias: [String]): CommonResult 10 | } 11 | 12 | type AllPageResult { 13 | code: Int 14 | message: String 15 | data: [OnePage] 16 | total: Int 17 | } 18 | 19 | type OnePage { 20 | id: Int 21 | name: String 22 | alias: String 23 | lastUpdateTime: String 24 | pageSortValue: String 25 | structure: String 26 | contents: [Content] 27 | } 28 | 29 | type Content { 30 | name: String 31 | alias: String 32 | value: String 33 | } 34 | 35 | input ContentInput { 36 | name: String 37 | alias: String 38 | value: String 39 | } 40 | 41 | type OnePageResult { 42 | code: Int 43 | message: String 44 | data: OnePage 45 | } 46 | 47 | input CreatePageRequest { 48 | name: String 49 | alias: String 50 | pageSortAlias: String 51 | structure: String 52 | contents: [ContentInput] 53 | } 54 | 55 | input UpdatePageRequest { 56 | id: Int 57 | name: String 58 | alias: String 59 | pageSortAlias: String 60 | structure: String 61 | contents: [ContentInput] 62 | } -------------------------------------------------------------------------------- /src/cms/graphqls/picGroup.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | # 查看所有组图信息 3 | findpGs(currentPage:Int, pageSize:Int):pGResults 4 | # 通过id查看图片组信息 5 | findpG(id: Int):pGResult 6 | } 7 | 8 | type Mutation { 9 | # 新增图片组的图片.id为图片组id 10 | addPicture(id:Int pic:InputPicture): CommonResult 11 | # 编辑图片组的图片 12 | updatePicture(pic:InputPictures): CommonResult 13 | # 删除图片组的图片 14 | delPicture(id:Int): CommonResult 15 | # 新增图片组 16 | addPicGroup(name:String): CommonResult 17 | # 删除图片组 18 | delPicGroup(id:Int): CommonResult 19 | # 修改图片组信息 20 | updatePicGroup(pG:InputpG): CommonResult 21 | } 22 | 23 | input InputpG { 24 | id:Int 25 | name: String 26 | } 27 | 28 | input InputPicture { 29 | name: String 30 | title: String 31 | Url: String 32 | sequence: Int 33 | } 34 | 35 | input InputPictures { 36 | id: Int 37 | name: String 38 | title: String 39 | Url: String 40 | sequence: Int 41 | } 42 | 43 | type pGResults { 44 | code: Int 45 | message: String 46 | total: Int 47 | data: [picGroup] 48 | } 49 | 50 | type picGroup { 51 | id: Int 52 | name: String 53 | } 54 | 55 | type pGResult { 56 | code: Int 57 | message: String 58 | data: picGroups 59 | } 60 | 61 | type picGroups { 62 | id: Int 63 | name: String 64 | pictures:[Pictures] 65 | } -------------------------------------------------------------------------------- /src/cms/graphqls/picture.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | findPicture(id: Int): findPictureResult 3 | } 4 | 5 | type findPictureResult { 6 | code: Int 7 | message: String 8 | data: Pictures 9 | } 10 | 11 | type Pictures { 12 | id: Int 13 | title: String 14 | name: String 15 | Url: String 16 | sequence: Int 17 | } -------------------------------------------------------------------------------- /src/cms/graphqls/user-message.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | # 查看用户id查看个人所有消息通知 3 | findAdvices(id: Int, pageNumber: Int, pageSize: Int): unsResult 4 | # 通过消息id查看消息详情 5 | findAdvice(id: Int): unResult 6 | # 查看该用户是否有未读消息 7 | findNoread(id: Int): readResult 8 | } 9 | 10 | type Mutation { 11 | # 通过id将一条消息设为已读 12 | readAdvice(id: Int): CommonResult 13 | # 将个人所有消息设为已读 14 | readAdvices(owner: Int): CommonResult 15 | # 通过id删除个人消息 16 | delAdvice(id: Int): CommonResult 17 | # 通过id删除多条个人消息 18 | delAdvices(ids: [Int]): CommonResult 19 | } 20 | 21 | type readResult { 22 | code:Int 23 | message: String 24 | data: Boolean 25 | } 26 | 27 | type unsResult { 28 | code: Int 29 | message: String 30 | total: Int 31 | data: [un] 32 | } 33 | 34 | type unResult { 35 | id: Int 36 | code: Int 37 | message: String 38 | content: String 39 | data: usernews 40 | } 41 | 42 | type un { 43 | id: Int 44 | title: String 45 | date: String 46 | content: String 47 | state: Boolean 48 | } 49 | 50 | type usernews { 51 | id: Int 52 | content: String 53 | title: String 54 | time: String 55 | } -------------------------------------------------------------------------------- /src/cms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities/article.entity'; 2 | export * from './entities/classify.entity'; 3 | export * from './entities/content.entity'; 4 | export * from './entities/page-sort.entity'; 5 | export * from './entities/page.entity'; 6 | export * from './entities/picture.entity'; 7 | export * from './entities/picture-group.entity'; 8 | export * from './entities/user-message.entity'; 9 | 10 | export * from './services/article.service'; 11 | export * from './services/classify.service'; 12 | export * from './services/page-sort.service'; 13 | export * from './services/page.service'; 14 | export * from './services/pic-group.service'; 15 | export * from './services/user-message.service'; 16 | 17 | export * from './cms.module'; -------------------------------------------------------------------------------- /src/cms/interfaces/article.interface.ts: -------------------------------------------------------------------------------- 1 | export interface InputArticle { 2 | title: string; 3 | username: string; 4 | owner: number; 5 | classify: { 6 | value: string; 7 | }; 8 | cover: string; 9 | abstract: string; 10 | content: string; 11 | top: number; 12 | hidden: boolean; 13 | status: number; 14 | source: string; 15 | sourceUrl: string; 16 | createAt: string; 17 | structure: string; 18 | artInfos: JSON; 19 | } 20 | 21 | export interface UpdateArticle { 22 | id: number; 23 | title: string; 24 | classify: { 25 | value: string; 26 | }; 27 | sourceUrl: string; 28 | cover: string; 29 | abstract: string; 30 | content: string; 31 | top: number; 32 | hidden: boolean; 33 | source: string; 34 | structure: string; 35 | modifyAt?: string; 36 | status?: number; 37 | artInfos: JSON; 38 | username: string; 39 | } 40 | 41 | export interface ArtResult { 42 | id: number; 43 | title: string; 44 | classify: { 45 | id: number; 46 | label: string; 47 | value: string; 48 | onlyChildrenArt: boolean; 49 | }; 50 | sourceUrl: string; 51 | cover: string; 52 | structure: string; 53 | abstract: string; 54 | content: string; 55 | top: number; 56 | source: string; 57 | username: string; 58 | artInfos: JSON; 59 | } -------------------------------------------------------------------------------- /src/cms/interfaces/classify.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreateClassify { 2 | label: string; 3 | value: string; 4 | parent: { value: string }; 5 | onlyChildrenArt: boolean; 6 | structure?: string; 7 | order?: number; 8 | itemJson?: JSON; 9 | } -------------------------------------------------------------------------------- /src/cms/interfaces/page-sort.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CreatePageSort { 2 | label: string; 3 | value: string; 4 | parent: { value: string }; 5 | structure: string; 6 | } -------------------------------------------------------------------------------- /src/cms/interfaces/page.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PageInput { 2 | name: string; 3 | alias: string; 4 | pageSortAlias: string; 5 | contents: { name: string, alias: string, value: string }[]; 6 | structure: string; 7 | } 8 | 9 | export interface PageUpdateInput { 10 | id: number; 11 | name: string; 12 | alias: string; 13 | contents: { 14 | name: string, 15 | alias: string, 16 | value: string 17 | }[]; 18 | pageSortAlias: string; 19 | structure: string; 20 | } -------------------------------------------------------------------------------- /src/cms/resolvers/article.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Query, Resolver, } from '@nestjs/graphql'; 2 | import { ArticleService } from '../services/article.service'; 3 | import { InputArticle, UpdateArticle } from '../interfaces/article.interface'; 4 | 5 | @Resolver() 6 | export class ArticleResolver { 7 | constructor( 8 | private readonly articleService: ArticleService 9 | ) { } 10 | 11 | @Query('getAllArticle') 12 | async getAllArticle(obj, body: { classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number }) { 13 | const result = await this.articleService.getAllArticle(body.classifyAlias, body.createdAt, body.endTime, body.title, body.username, body.top, body.pageNumber, body.pageSize); 14 | return { code: 200, message: '查询成功!', data: result.exist, total: result.total }; 15 | } 16 | 17 | @Query('userGetArticles') 18 | async userGetArticles(obj, body: { alias: string, pageNumber: number, pageSize: number }) { 19 | const result = await this.articleService.userGetArticles(body.alias, body.pageNumber, body.pageSize); 20 | return { code: 200, message: '查询成功!', data: result.exist, total: result.total }; 21 | } 22 | 23 | @Query('getRecycleArticle') 24 | async getRecycleArticle(obj, body: { classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number }) { 25 | const result = await this.articleService.getRecycleArticle(body.classifyAlias, body.createdAt, body.endTime, body.title, body.username, body.top, body.pageNumber, body.pageSize); 26 | return { code: 200, message: '查询成功!', data: result.exist, total: result.total }; 27 | } 28 | 29 | @Query('getCheckArticle') 30 | async getCheckArticle(obj, body: { classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number }) { 31 | const result = await this.articleService.getCheckArticle(body.classifyAlias, body.createdAt, body.endTime, body.title, body.username, body.top, body.pageNumber, body.pageSize); 32 | return { code: 200, message: '查询成功!', data: result.exist, total: result.total }; 33 | } 34 | 35 | @Query('getArticleById') 36 | async getArticleById(obj, body: { id: number }) { 37 | const data = await this.articleService.getArticleById(body.id); 38 | return { code: 200, message: '查询成功!', data }; 39 | } 40 | 41 | @Mutation('createArticle') 42 | async createArticle(obj, body: { article: InputArticle }, context) { 43 | if (!context.user.roles.length || context.user.roles[0].id === 2) { 44 | body.article.status = 1; 45 | } 46 | if (context.user.banned || context.user.recycling) { 47 | return { code: 403, message: '您的账户暂不可用,请联系管理员' }; 48 | } 49 | await this.articleService.createArticle(body.article, context.user.id); 50 | return { code: 200, message: '创建成功!' }; 51 | } 52 | 53 | @Mutation('updateArticle') 54 | async updateArticle(obj, body: { article: UpdateArticle }, context) { 55 | if (context.user.banned || context.user.recycling) { 56 | return { code: 403, message: '您的账户暂不可用,请联系管理员' }; 57 | } 58 | await this.articleService.updateArticle(body.article, context.user.id); 59 | return { code: 200, message: '修改成功!' }; 60 | } 61 | 62 | @Mutation('recycleArticleByIds') 63 | async recycleArticleByIds(obj, body: { ids: number[] }) { 64 | await this.articleService.recycleArticleByIds(body.ids); 65 | return { code: 200, message: '删除成功!' }; 66 | } 67 | 68 | @Mutation('deleteArticleByIds') 69 | async deleteArticleByIds(obj, body: { ids: number[] }) { 70 | await this.articleService.deleteArticleByIds(body.ids); 71 | return { code: 200, message: '删除成功!' }; 72 | } 73 | 74 | @Mutation('recoverArticleByIds') 75 | async recoverArticleByIds(obj, body: { ids: number[] }) { 76 | await this.articleService.recoverArticleByIds(body.ids); 77 | return { code: 200, message: '恢复成功!' }; 78 | } 79 | 80 | @Mutation('auditArticle') 81 | async auditArticle(obj, body: { ids: number[], op: number, refuseReason: string }) { 82 | await this.articleService.auditArticle(body.ids, body.op, body.refuseReason); 83 | return { code: 200, message: '审核成功!' }; 84 | } 85 | 86 | 87 | } -------------------------------------------------------------------------------- /src/cms/resolvers/classify.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args, Query } from '@nestjs/graphql'; 2 | import { Inject } from '@nestjs/common'; 3 | import { ClassifyService } from '../services/classify.service'; 4 | import { CreateClassify } from '../interfaces/classify.interface'; 5 | import { Classify } from '../entities/classify.entity'; 6 | 7 | @Resolver() 8 | export class ClassifyResolver { 9 | 10 | constructor( 11 | private readonly classifyService: ClassifyService, 12 | ) { } 13 | 14 | @Query('getAllClassify') 15 | async getAllClassify(obj, body: { id: number }) { 16 | const result = await this.classifyService.getAllClassify(body.id); 17 | return { code: 200, message: '查询成功!', data: result }; 18 | } 19 | 20 | @Query('getOneClassify') 21 | async getOneClassify(obj, body: { value: string }) { 22 | const result = await this.classifyService.getOneClassify(body.value); 23 | return { code: 200, message: '查询成功!', data: result }; 24 | } 25 | 26 | @Query('getParentClassify') 27 | async getParentClassify(obj, body: { id: number }) { 28 | const data = await this.classifyService.getParentClassify(body.id); 29 | return { code: 200, message: '查询成功!', data }; 30 | } 31 | 32 | @Mutation('addClassify') 33 | async addClassify(obj, body: { classify: CreateClassify }) { 34 | await this.classifyService.addClassify(body.classify); 35 | return { code: 200, message: '创建分类成功' }; 36 | } 37 | 38 | @Mutation('deleteClassify') 39 | async deleteClassify(obj, body: { id: number }) { 40 | await this.classifyService.delClassify(body.id); 41 | return { code: 200, message: '删除分类成功' }; 42 | } 43 | 44 | @Mutation('updateClassify') 45 | async updateClassify(obj, body: { classify: Classify }) { 46 | await this.classifyService.updateClassify(body.classify); 47 | return { code: 200, message: '更新分类成功' }; 48 | } 49 | 50 | @Mutation('mobileArticles') 51 | async mobileArticles(obj, body: { oldId: number, newId: number }) { 52 | await this.classifyService.mobileArticles(body.oldId, body.newId); 53 | return { code: 200, message: '移动文章成功!' }; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/cms/resolvers/index.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Query, Resolver, } from '@nestjs/graphql'; 2 | import { Index } from '../entities/index.entity'; 3 | import { IndexService } from '../services/index.service'; 4 | 5 | @Resolver() 6 | export class IndexResolver { 7 | constructor( 8 | private readonly indexService: IndexService 9 | ) { } 10 | 11 | @Query('getIndex') 12 | async getIndex() { 13 | const result = await this.indexService.getIndex(); 14 | return { code: 200, message: '查询成功!', data: result }; 15 | } 16 | 17 | @Mutation('updateIndex') 18 | async updateIndex(obj, body: { index: Index }, context) { 19 | await this.indexService.updateIndex(body.index); 20 | return { code: 200, message: '修改成功!' }; 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /src/cms/resolvers/page-sort.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args, Query } from '@nestjs/graphql'; 2 | import { Inject } from '@nestjs/common'; 3 | import { PageSortService } from '../services/page-sort.service'; 4 | import { CreatePageSort } from '../interfaces/page-sort.interface'; 5 | import { PageSort } from '../entities/page-sort.entity'; 6 | 7 | @Resolver() 8 | export class PageSortResolver { 9 | 10 | constructor( 11 | @Inject(PageSortService) private readonly psService: PageSortService, 12 | ) { } 13 | 14 | @Query('getAllPageSort') 15 | async getAllPageSort() { 16 | const data = await this.psService.getAllPageSort(); 17 | return { code: 200, message: '查询成功!', data }; 18 | } 19 | 20 | @Query('getOnePageSort') 21 | async getOnePageSort(obj, body: { alias: string }) { 22 | const data = await this.psService.getOnePageSort(body.alias); 23 | return { code: 200, message: '查询成功!', data }; 24 | } 25 | 26 | @Mutation('createPageSort') 27 | async createPageSort(obj, body: { pageSort: CreatePageSort }) { 28 | await this.psService.createPageSort(body.pageSort); 29 | return { code: 200, message: '创建页面分类成功!' }; 30 | } 31 | 32 | @Mutation('updatePageSort') 33 | async updatePageSort(obj, body: { pageSort: PageSort }) { 34 | await this.psService.updatePageSort(body.pageSort); 35 | return { code: 200, message: '修改成功!' }; 36 | } 37 | 38 | @Mutation('deletePageSort') 39 | async deletePageSort(obj, body: { id: number }) { 40 | await this.psService.deletePageSort(body.id); 41 | return { code: 200, message: '删除成功!' }; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/cms/resolvers/page.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Mutation, Args, Query } from '@nestjs/graphql'; 2 | import { Inject } from '@nestjs/common'; 3 | import { PageService } from '../services/page.service'; 4 | import { PageInput, PageUpdateInput } from '../interfaces/page.interface'; 5 | import { Page } from '../entities/page.entity'; 6 | 7 | @Resolver() 8 | export class PageResolver { 9 | constructor( 10 | @Inject(PageService) private readonly pageService: PageService, 11 | ) { } 12 | 13 | @Query('getAllPage') 14 | async getAllPage(obj, body: { name: string, alias: string, pageNumber: number, pageSize: number}) { 15 | const result = await this.pageService.getAllPage(body.name, body.alias, body.pageNumber, body.pageSize); 16 | return { code: 200, message: '查询成功!', data: result.data, total: result.total }; 17 | } 18 | 19 | @Query('getOnePage') 20 | async getOnePage(obj, body: { alias: string }) { 21 | const a = await this.pageService.getOnePage(body.alias); 22 | return { code: 200, message: '查询成功!', data: a }; 23 | } 24 | 25 | @Mutation('createPage') 26 | async createPage(obj, body: { page: PageInput }) { 27 | await this.pageService.createPage(body.page); 28 | return { code: 200, message: '创建成功!' }; 29 | } 30 | 31 | @Mutation('updatePage') 32 | async updatePage(obj, body: { page: PageUpdateInput }) { 33 | await this.pageService.updatePage(body.page); 34 | return { code: 200, message: '修改成功!' }; 35 | } 36 | 37 | @Mutation('deletePage') 38 | async deletePage(obj, body: { alias: [string] }) { 39 | await this.pageService.deletePage(body.alias); 40 | return { code: 200, message: '删除成功!' }; 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/cms/resolvers/pic-group.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { Resolver, Mutation, Query } from '@nestjs/graphql'; 3 | import { PictureGroup } from '../entities/picture-group.entity'; 4 | import { Picture } from '../entities/picture.entity'; 5 | import { PicGroupService } from '../services/pic-group.service'; 6 | 7 | @Resolver('picGroupResolver') 8 | // tslint:disable-next-line:class-name 9 | export class picGroupResolver { 10 | constructor( 11 | @Inject(PicGroupService) 12 | private readonly pGService: PicGroupService, 13 | ) { } 14 | 15 | @Query('findpGs') 16 | async findpGs(req, body: { currentPage: number, pageSize: number }) { 17 | const data = (await this.pGService.findpGs(body.currentPage, body.pageSize)).data; 18 | const total = (await this.pGService.findpGs(body.currentPage, body.pageSize)).total; 19 | return { code: 200, message: '查询成功!', total, data }; 20 | } 21 | 22 | @Query('findpG') 23 | async findpG(req, abc) { 24 | const data = await this.pGService.findOne(abc.id); 25 | return { code: 200, message: '查询成功!', data }; 26 | } 27 | 28 | @Mutation('addPicGroup') 29 | async save(req, abc) { 30 | await this.pGService.addPicGroup(abc.name); 31 | return { code: 200, message: '创建成功' }; 32 | } 33 | 34 | 35 | @Mutation('addPicture') 36 | async addPicture(req, body: { pic: Picture, id: number }) { 37 | await this.pGService.addPicture(body.id, body.pic); 38 | return { code: 200, message: '添加成功' }; 39 | } 40 | 41 | @Mutation('updatePicture') 42 | async updatePicture(req, body: { pic: Picture }) { 43 | await this.pGService.updatePicture(body.pic); 44 | return { code: 200, message: '修改成功' }; 45 | } 46 | 47 | @Mutation('delPicture') 48 | async delPicture(req, abc) { 49 | await this.pGService.delPicture(abc.id); 50 | return { code: 200, message: '删除成功' }; 51 | } 52 | 53 | @Mutation('delPicGroup') 54 | async delPicGroup(req, abc) { 55 | await this.pGService.delPicGroup(abc.id); 56 | return { code: 200, message: '删除成功' }; 57 | } 58 | 59 | @Mutation('updatePicGroup') 60 | async updatePicGroup(req, body: { pG: PictureGroup }) { 61 | await this.pGService.updatePicGroup(body.pG); 62 | return { code: 200, message: '修改成功' }; 63 | } 64 | 65 | @Query('findPicture') 66 | async findPicture(req, abc) { 67 | const data = await this.pGService.findPicture(abc.id); 68 | return { code: 200, message: '查询成功!', data }; 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /src/cms/resolvers/user-message.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, Mutation } from '@nestjs/graphql'; 2 | import { Inject } from '@nestjs/common'; 3 | import { UserMessageService } from '../services/user-message.service'; 4 | 5 | @Resolver() 6 | export class UserMessageResolver { 7 | constructor( 8 | @Inject(UserMessageService) 9 | private readonly adService: UserMessageService, 10 | ) { } 11 | 12 | 13 | @Query('findNoread') 14 | async findNoread(req, abc) { 15 | const data = await this.adService.findNoread(abc.id); 16 | return { code: 200, message: '查询成功!', data }; 17 | } 18 | 19 | // 通过用户id查询所有个人消息 20 | @Query('findAdvices') 21 | async findAll(req, body: { id: number, pageNumber: number, pageSize: number }) { 22 | const data = (await this.adService.findAll(body.id, body.pageNumber, body.pageSize)).data; 23 | const total = (await this.adService.findAll(body.id, body.pageNumber, body.pageSize)).total; 24 | return { code: 200, message: '查询成功!', total, data }; 25 | } 26 | 27 | // 通过id查看个人消息 28 | @Query('findAdvice') 29 | async findOne(req, abc) { 30 | const data = await this.adService.findOne(abc.id); 31 | return { code: 200, message: '查询成功!', data }; 32 | } 33 | 34 | // 将消息设为已读 35 | @Mutation('readAdvice') 36 | async readOne(req, abc) { 37 | await this.adService.readOne(abc.id); 38 | return { code: 200, message: '设置成功!' }; 39 | } 40 | 41 | // 通过用户id将所有消息设为已读 42 | @Mutation('readAdvices') 43 | async readAll(req, abc) { 44 | await this.adService.readAll(abc.owner); 45 | return { code: 200, message: '设置成功!' }; 46 | } 47 | 48 | // 删除一条消息 49 | @Mutation('delAdvice') 50 | async del(req, abc) { 51 | await this.adService.del(abc.id); 52 | return { code: 200, message: '删除消息成功!' }; 53 | } 54 | 55 | // 通过id批量删除 56 | @Mutation('delAdvices') 57 | async dels(req, body: { ids: [number] }) { 58 | await this.adService.dels(body.ids); 59 | return { code: 200, message: '删除多条消息成功!' }; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/cms/services/article.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, HttpException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository, TreeRepository } from 'typeorm'; 4 | import * as moment from 'moment'; 5 | import { Article } from '../entities/article.entity'; 6 | import { Classify } from '../entities/classify.entity'; 7 | import { InputArticle, UpdateArticle, ArtResult } from '../interfaces/article.interface'; 8 | import { ClassifyService } from './classify.service'; 9 | import { UserService, User } from 'src/user'; 10 | const _ = require('underscore'); 11 | 12 | 13 | @Injectable() 14 | export class ArticleService { 15 | 16 | constructor( 17 | @InjectRepository(Article) private readonly artRepo: Repository
, 18 | @InjectRepository(Classify) private readonly claRepo: TreeRepository, 19 | @Inject(UserService) 20 | private readonly userService: UserService, 21 | @Inject(ClassifyService) private readonly classifyService: ClassifyService, 22 | @InjectRepository(User) 23 | private readonly userRepo: Repository, 24 | ) { } 25 | 26 | 27 | /** 28 | * 创建文章 29 | * 30 | * @param art 文章实体 31 | */ 32 | async createArticle(art: InputArticle, owner: number) { 33 | const classify = await this.claRepo.findOne({ value: art.classify.value }); 34 | if (!classify) { 35 | throw new HttpException('该文章分类不存在!', 404); 36 | } 37 | art.owner = owner; 38 | art.classify = classify; 39 | await this.artRepo.save(this.artRepo.create(art)); 40 | } 41 | 42 | /** 43 | * 修改文章 44 | * 45 | * @param art 修改文章实体(有id) 46 | * 47 | */ 48 | async updateArticle(art: UpdateArticle, owner: number) { 49 | try { 50 | const article = await this.artRepo.findOne(art.id); 51 | if (!article) { 52 | throw new HttpException('该文章不存在!', 404); 53 | } 54 | const classify = await this.claRepo.findOne({ value: art.classify.value }); 55 | if (!classify) { 56 | throw new HttpException('该文章分类不存在!', 404); 57 | } 58 | const user = await this.userRepo.findOne({ 59 | where: { id: owner }, 60 | relations: ['roles'] 61 | }); 62 | if (user.roles.length && user.roles[0].id === 1) { 63 | // 改为待审核状态 64 | article.status = 0; 65 | } 66 | art.classify = classify; 67 | art.modifyAt = moment().format('YYYY-MM-DD HH:mm:ss'); 68 | await this.artRepo.save(this.artRepo.create(art)); 69 | } catch (error) { 70 | throw new HttpException(error.toString(), 500); 71 | } 72 | } 73 | 74 | 75 | /** 76 | * 批量将文章丢入回收站 77 | * 78 | * @param ids 文章id数组 79 | */ 80 | async recycleArticleByIds(ids: number[]) { 81 | const articles: number[] = []; 82 | await this.artRepo.findByIds(ids).then(art => { 83 | art.map((key, value) => { 84 | articles.push(key.id); 85 | }); 86 | }); 87 | const noExist = _.difference(ids, articles); 88 | if (noExist.length > 0) { 89 | throw new HttpException(`id为${noExist}的文章不存在`, 404); 90 | } 91 | await this.artRepo.update(ids, { recycling: true }); 92 | } 93 | 94 | /** 95 | * 批量永久删除文章 96 | * 97 | * @param ids 文章id数组 98 | * 99 | */ 100 | async deleteArticleByIds(ids: number[]) { 101 | const articles: number[] = []; 102 | await this.artRepo.findByIds(ids).then(art => { 103 | art.map((key, value) => { 104 | articles.push(key.id); 105 | }); 106 | }); 107 | const noExist = _.difference(ids, articles); 108 | if (noExist.length > 0) { 109 | throw new HttpException(`id为${noExist}的文章不存在`, 404); 110 | } 111 | await this.artRepo.delete(ids); 112 | } 113 | 114 | 115 | /** 116 | * 批量恢复回收站中的文章 117 | * 118 | * @param ids 文章id数组 119 | */ 120 | async recoverArticleByIds(ids: number[]) { 121 | const articles: number[] = []; 122 | await this.artRepo.findByIds(ids).then(art => { 123 | art.map((key, value) => { 124 | articles.push(key.id); 125 | }); 126 | }); 127 | const noExist = _.difference(ids, articles); 128 | if (noExist.length > 0) { 129 | throw new HttpException(`id为${noExist}的文章不存在`, 404); 130 | } 131 | await this.artRepo.update(ids, { recycling: false }); 132 | } 133 | 134 | 135 | /** 136 | * 批量审核文章 137 | * 138 | * @param ids 审核文章id数组 139 | * @param status 审核操作.1:通过 2:拒绝 140 | * @param refuseReason 拒绝原因(拒绝文章时需输入) 141 | */ 142 | async auditArticle(ids: number[], op: number, refuseReason: string) { 143 | const articles: number[] = []; 144 | const arts = await this.artRepo.findByIds(ids); 145 | arts.forEach((key, value) => { 146 | articles.push(key.id); 147 | }); 148 | const noExist = _.difference(ids, articles); 149 | if (noExist.length > 0) { 150 | throw new HttpException(`id为${noExist}的文章不存在`, 404); 151 | } 152 | switch (op) { 153 | case 1: 154 | await this.artRepo.update(ids, { status: op }); 155 | break; 156 | case 2: 157 | await this.artRepo.update(ids, { status: op, refuseReason }); 158 | break; 159 | default: 160 | throw new HttpException('status参数错误', 405); 161 | } 162 | } 163 | 164 | /** 165 | * 后台搜索所有文章 166 | * 167 | * @param classifyId 文章分类id 168 | * @param createdAt 开始时间 169 | * @param endTime 截止时间 170 | * @param title 文章标题 171 | * @param username 文章作者用户名 172 | * @param top 是否置顶 173 | * @param pageNumber 页数 174 | * @param pageSize 每页显示数量 175 | */ 176 | // tslint:disable-next-line:max-line-length 177 | async getAllArticle(classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number) { 178 | const sqb = this.artRepo.createQueryBuilder('article') 179 | .where('article.status = :status', { status: 1 }) 180 | .andWhere('article.recycling = false') 181 | .leftJoinAndSelect('article.classify', 'classify'); 182 | if (classifyAlias) { 183 | const cla = await this.claRepo.findOne({ where: { value: classifyAlias } }); 184 | if (!cla) { 185 | throw new HttpException('该分类不存在!', 404); 186 | } 187 | sqb.andWhere('article.classify = :classify', { classify: cla.id }); 188 | } 189 | if (title) { 190 | sqb.andWhere('article.title Like :title', { title: `%${title}%` }); 191 | } 192 | if (username) { 193 | sqb.andWhere('article.username = :username', { username }); 194 | } 195 | if (createdAt) { 196 | const min = new Date(createdAt); 197 | sqb.andWhere('article.createdAt > :start', { start: min }); 198 | } 199 | if (endTime) { 200 | const max = new Date(endTime); 201 | sqb.andWhere('article.createdAt < :end', { end: max }); 202 | } 203 | if (top) { 204 | sqb.andWhere('article.top In (1,2) '); 205 | } 206 | if (top === false) { 207 | sqb.andWhere('article.top = :top', { top: 0 }); 208 | } 209 | // tslint:disable-next-line:max-line-length 210 | const result = await sqb.skip(pageSize * (pageNumber - 1)).take(pageSize).orderBy({ 'article.top': 'DESC', 'article.modifyAt': 'DESC' }).getManyAndCount(); 211 | const exist: ArtResult[] = []; 212 | for (const i of result[0]) { 213 | const classify = await this.claRepo.findOne(i.classify.id); 214 | const a = { 215 | id: i.id, 216 | title: i.title, 217 | classify, 218 | sourceUrl: i.sourceUrl, 219 | cover: i.cover, 220 | abstract: i.abstract, 221 | content: i.content, 222 | top: i.top, 223 | source: i.source, 224 | username, 225 | status: i.status, 226 | recycling: i.recycling, 227 | createdAt: i.createdAt, 228 | modifyAt: i.modifyAt, 229 | views: i.views, 230 | like: i.like, 231 | artInfos: i.artInfos, 232 | keywords: i.keywords, 233 | structure: i.structure, 234 | hidden: i.hidden 235 | }; 236 | exist.push(a); 237 | } 238 | return { exist, total: result[1] }; 239 | } 240 | 241 | async userGetArticles(alias: string, pageNumber: number, pageSize: number) { 242 | const cla = await this.claRepo.findOne({ where: { value: alias, relations: ['parent'] } }); 243 | const ids: number[] = []; 244 | if (cla.onlyChildrenArt) { 245 | const a = await this.classifyService.getAllClassifyIds(cla.id); 246 | const b = a.splice(a.indexOf(cla.id), 1); 247 | for (const i of b) { 248 | ids.push(i); 249 | } 250 | } 251 | else { 252 | const a = await this.classifyService.getAllClassifyIds(cla.id); 253 | for (const i of a) { 254 | ids.push(i); 255 | } 256 | } 257 | const sqb = this.artRepo.createQueryBuilder('article') 258 | .where('article.status = :status', { status: 1 }) 259 | .andWhere('article.recycling = false') 260 | .andWhere('article.hidden = false') 261 | .andWhere('article.classify IN(:...ids)', { ids }) 262 | .leftJoinAndSelect('article.classify', 'classify'); 263 | // tslint:disable-next-line:max-line-length 264 | const result = await sqb.skip(pageSize * (pageNumber - 1)).take(pageSize).orderBy({ 'article.top': 'DESC', 'article.modifyAt': 'DESC' }).getManyAndCount(); 265 | const exist = []; 266 | for (const i of result[0]) { 267 | const classify = await this.claRepo.findOne(i.classify); 268 | const a = { 269 | id: i.id, 270 | title: i.title, 271 | classify, 272 | cover: i.cover, 273 | abstract: i.abstract, 274 | content: i.content, 275 | createAt: i.createdAt, 276 | createdAt: i.createdAt, 277 | keywords: i.keywords, 278 | like: i.like, 279 | sourceUrl: i.sourceUrl, 280 | source: i.source 281 | }; 282 | exist.push(a); 283 | } 284 | return { total: result[1], exist }; 285 | } 286 | 287 | /** 288 | * 搜索回收站中文章 289 | * 290 | */ 291 | // tslint:disable-next-line:max-line-length 292 | async getRecycleArticle(classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number) { 293 | const sqb = this.artRepo.createQueryBuilder('article') 294 | .where('article.recycling = :recycling', { recycling: true }) 295 | .leftJoinAndSelect('article.classify', 'classify'); 296 | if (classifyAlias) { 297 | const cla = await this.claRepo.findOne({ where: { value: classifyAlias } }); 298 | if (!cla) { 299 | throw new HttpException('该分类不存在!', 404); 300 | } 301 | sqb.andWhere('article.classify = :classify', { classify: cla.id }); 302 | } 303 | if (title) { 304 | sqb.andWhere('article.title Like :title', { title: `%${title}%` }); 305 | } 306 | if (username) { 307 | sqb.andWhere('article.username = :username', { username }); 308 | } 309 | if (createdAt) { 310 | const min = new Date(createdAt); 311 | const max = endTime ? new Date(endTime) : new Date(); 312 | sqb.andWhere('article.createdAt > :start', { start: min }); 313 | sqb.andWhere('article.createdAt < :end', { end: max }); 314 | } 315 | if (top) { 316 | sqb.andWhere('article.top In (1,2) '); 317 | } 318 | if (top === false) { 319 | sqb.andWhere('article.top = :top', { top: 0 }); 320 | } 321 | // tslint:disable-next-line:max-line-length 322 | const result = await sqb.skip(pageSize * (pageNumber - 1)).take(pageSize).orderBy({ 'article.top': 'DESC', 'article.modifyAt': 'DESC' }).getMany(); 323 | const exist: ArtResult[] = []; 324 | const total = await sqb.getCount(); 325 | for (const i of result) { 326 | const classify = await this.claRepo.findOne(i.classify.id); 327 | const a = { 328 | id: i.id, 329 | title: i.title, 330 | classify, 331 | sourceUrl: i.sourceUrl, 332 | cover: i.cover, 333 | abstract: i.abstract, 334 | content: i.content, 335 | top: i.top, 336 | source: i.source, 337 | username, 338 | status: i.status, 339 | views: i.views, 340 | like: i.like, 341 | recycling: i.recycling, 342 | createdAt: i.createdAt, 343 | modifyAt: i.modifyAt, 344 | structure: i.structure, 345 | artInfos: i.artInfos 346 | }; 347 | exist.push(a); 348 | } 349 | return { exist, total }; 350 | } 351 | 352 | 353 | /** 354 | * 获取文章详情 355 | * 356 | * @param id 文章id 357 | */ 358 | async getArticleById(id: number) { 359 | const artQb = await this.artRepo.createQueryBuilder('art') 360 | .leftJoinAndSelect('art.classify', 'classify'); 361 | 362 | const art = await artQb.where('art.id = :id', { id }).getOne(); 363 | art.views++; 364 | const classifyId = art.classify.id; 365 | const pre = await this.artRepo.createQueryBuilder() 366 | // tslint:disable-next-line:max-line-length 367 | .where('"id" = (select max(id) from public.article where "id" < :id and "classifyId" =:classifyId and "status" =:status and "recycling" =:recycling) ', 368 | { id, classifyId, status: 1, recycling: false } 369 | ) 370 | .getOne(); 371 | const next = await this.artRepo.createQueryBuilder() 372 | // tslint:disable-next-line:max-line-length 373 | .where('"id" = (select min(id) from public.article where "id" > :id and "classifyId" =:classifyId and "status" =:status and "recycling" =:recycling) ', 374 | { id, classifyId, status: 1, recycling: false } 375 | ) 376 | .getOne(); 377 | const data = await this.artRepo.save(this.artRepo.create(art)); 378 | return { pre: pre ? pre.id : undefined, current: data, next: next ? next.id : undefined }; 379 | } 380 | 381 | /** 382 | * 383 | * 搜索获取待审核文章 384 | * 385 | */ 386 | // tslint:disable-next-line:max-line-length 387 | async getCheckArticle(classifyAlias: string, createdAt: string, endTime: string, title: string, username: string, top: boolean, pageNumber: number, pageSize: number) { 388 | const sqb = this.artRepo.createQueryBuilder('article') 389 | .where('article.recycling = :recycling', { recycling: false }) 390 | .andWhere('article.status = :status', { status: 0 }) 391 | .leftJoinAndSelect('article.classify', 'classify'); 392 | if (classifyAlias) { 393 | const cla = await this.claRepo.findOne({ where: { value: classifyAlias } }); 394 | if (!cla) { 395 | throw new HttpException('该分类不存在!', 404); 396 | } 397 | sqb.andWhere('article.classify = :classify', { classify: cla.id }); 398 | } 399 | if (title) { 400 | sqb.andWhere('article.title Like :title', { title: `%${title}%` }); 401 | } 402 | if (username) { 403 | sqb.andWhere('article.username = :username', { username }); 404 | } 405 | if (createdAt) { 406 | const min = new Date(createdAt); 407 | const max = endTime ? new Date(endTime) : new Date(); 408 | sqb.andWhere('article.createdAt > :start', { start: min }); 409 | sqb.andWhere('article.createdAt < :end', { end: max }); 410 | } 411 | if (top) { 412 | sqb.andWhere('article.top In (1,2) '); 413 | } 414 | if (top === false) { 415 | sqb.andWhere('article.top = :top', { top: 0 }); 416 | } 417 | const result = await sqb.skip(pageSize * (pageNumber - 1)).take(pageSize).orderBy({ 'article.modifyAt': 'ASC' }).getMany(); 418 | const exist: ArtResult[] = []; 419 | const total = await sqb.getCount(); 420 | for (const i of result) { 421 | const classify = await this.claRepo.findOne(i.classify.id); 422 | const a = { 423 | id: i.id, 424 | title: i.title, 425 | classify, 426 | sourceUrl: i.sourceUrl, 427 | cover: i.cover, 428 | abstract: i.abstract, 429 | content: i.content, 430 | top: i.top, 431 | source: i.source, 432 | createdAt: i.createdAt, 433 | username, 434 | status: i.status, 435 | recycling: i.recycling, 436 | views: i.views, 437 | like: i.like, 438 | modifyAt: i.modifyAt, 439 | keywords: i.keywords, 440 | structure: i.structure, 441 | artInfos: i.artInfos 442 | }; 443 | exist.push(a); 444 | } 445 | return { exist, total }; 446 | } 447 | 448 | 449 | private addDate(dates, days) { 450 | if (days === undefined || days === '') { 451 | days = 1; 452 | } 453 | const date = new Date(dates); 454 | date.setDate(date.getDate() + days); 455 | const month = date.getMonth() + 1; 456 | const day = date.getDate(); 457 | return date.getFullYear() + '-' + this.getFormatDate(month) + '-' + this.getFormatDate(day); 458 | } 459 | 460 | private getFormatDate(arg) { 461 | if (arg === undefined || arg === '') { 462 | return ''; 463 | } 464 | let re = arg + ''; 465 | if (re.length < 2) { 466 | re = '0' + re; 467 | } 468 | return re; 469 | } 470 | 471 | } -------------------------------------------------------------------------------- /src/cms/services/classify.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException } from '@nestjs/common'; 2 | import { CreateClassify } from '../interfaces/classify.interface'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Classify } from '../entities/classify.entity'; 5 | import { Article } from '../entities/article.entity'; 6 | import { TreeRepository, Repository, In } from 'typeorm'; 7 | 8 | @Injectable() 9 | export class ClassifyService { 10 | constructor( 11 | @InjectRepository(Classify) private readonly claRepository: TreeRepository, 12 | @InjectRepository(Article) private readonly artRepository: Repository
, 13 | ) { } 14 | 15 | /** 16 | * 新增文章分类 17 | * 18 | * @param classify 新增分类实体 19 | */ 20 | async addClassify(classify: CreateClassify) { 21 | try { 22 | const ignore = await this.claRepository.count(); 23 | if (!classify.parent.value || ignore <= 0) { 24 | await this.claRepository.save(this.claRepository.create({ label: '总分类', value: 'root', onlyChildrenArt: true })); 25 | return { code: 200, message: '创建成功' }; 26 | } 27 | if (classify.parent) { 28 | const exist = await this.claRepository.findOne({ value: classify.parent.value }); 29 | if (!exist) { 30 | return { code: 405, message: '当前分类父节点不存在' }; 31 | } 32 | classify.parent = exist; 33 | } 34 | if (classify.value !== 'root') { 35 | const result = await this.claRepository.findOne({ where: { value: classify.value } }); 36 | if (result) { 37 | throw new HttpException('别名重复!', 406); 38 | } 39 | } 40 | await this.claRepository.save(await this.claRepository.create(classify)); 41 | 42 | } catch (err) { 43 | throw new HttpException(err.toString(), 500); 44 | } 45 | } 46 | 47 | /** 48 | * 删除文章分类 49 | * 50 | * @param id 文章分类id 51 | */ 52 | async delClassify(id: number) { 53 | const classify: Classify = await this.claRepository.findOne({ id }); 54 | if (!classify) { 55 | return { code: 404, message: '当前分类不存在' }; 56 | } 57 | const array = await this.getAllClassifyIds(id); 58 | const articles = await this.artRepository.count({ where: { classify: In(array) } }); 59 | if (articles > 0) { 60 | throw new HttpException('当前分类下有文章,不能删除', 403); 61 | } 62 | array.splice(array.indexOf(id), 1); 63 | if (array.length) { 64 | throw new HttpException('当前分类下有子分类,不能删除', 403); 65 | } 66 | await this.claRepository.remove(classify); 67 | return { code: 200, message: '删除成功' }; 68 | } 69 | 70 | /** 71 | * 获取该分类所有子分类id 72 | * 73 | * @param idNum 指定分类id 74 | */ 75 | async getAllClassifyIds(idNum: number): Promise { 76 | const array: number[] = []; 77 | const classify = await this.claRepository.findOne({ id: idNum }); 78 | await this.claRepository.findDescendants(classify).then(a => { 79 | if (a) { 80 | a.map(a => { 81 | array.push(a.id); 82 | }); 83 | } 84 | }); 85 | return array; 86 | } 87 | 88 | /** 89 | * 修改分类 90 | * 91 | * @param classify 被修改分类实体 92 | */ 93 | async updateClassify(classify: Classify) { 94 | const exist = await this.claRepository.findOne({ id: classify.id }); 95 | if (!exist) { 96 | return { code: 404, message: '当前分类不存在!' }; 97 | } 98 | if (classify.value && classify.value !== exist.value) { 99 | if (await this.claRepository.findOne({ where: { value: classify.value } })) { 100 | throw new HttpException('该分类别名已存在!', 409); 101 | } 102 | } 103 | const parent = await this.claRepository.findOne({ value: classify.parent.value }); 104 | if (!parent) { 105 | throw new HttpException('该上级分类不存在', 404); 106 | } 107 | try { 108 | classify.parent = parent; 109 | await this.claRepository.save(await this.claRepository.create(classify)); 110 | } catch (err) { 111 | throw new HttpException(err.toString(), 500); 112 | } 113 | } 114 | 115 | 116 | /** 117 | * 查询所有分类 118 | * 119 | * @param id 不传时查询所有分类,传了则只查询该分类及其子分类 120 | */ 121 | async getAllClassify(id: number) { 122 | if (id) { 123 | const exist = await this.claRepository.findOne(id); 124 | if (!exist) { 125 | throw new HttpException('该分类不存在!', 404); 126 | } 127 | return await this.claRepository.findDescendantsTree(exist); 128 | } else { 129 | return await this.claRepository.findTrees(); 130 | } 131 | } 132 | 133 | /** 134 | * 查询分类详情 135 | * 136 | * @param id 指定分类id 137 | */ 138 | async getOneClassify(value: string) { 139 | const exist = await this.claRepository.findOne({ where: { value } }); 140 | if (!exist) { 141 | throw new HttpException('该分类不存在!', 404); 142 | } 143 | const data = await this.claRepository.findAncestorsTree(exist); 144 | return data; 145 | } 146 | 147 | /** 148 | * 获取上级分类 149 | * 150 | * @param id 指定分类id 151 | */ 152 | async getParentClassify(id: number) { 153 | const exist = await this.claRepository.findOne({ id }); 154 | if (!exist) { 155 | throw new HttpException('该分类不存在!', 404); 156 | } 157 | const data = await this.claRepository.findAncestorsTree(exist); 158 | return { code: 200, message: '查询成功!', data: [data] }; 159 | } 160 | 161 | /** 162 | * 移动分类下的文章至另一分类 163 | * 164 | * @param classifyId 原分类id 165 | * @param newClassifyId 需要移至分类id 166 | */ 167 | async mobileArticles(classifyId: number, newClassifyId: number) { 168 | const exist = await this.claRepository.findOne({ where: { id: classifyId } }); 169 | if (!exist) { 170 | return { code: 404, message: '原分类不存在!' }; 171 | } 172 | const newClassify = await this.claRepository.findOne({ where: { id: newClassifyId } }); 173 | if (!newClassify) { 174 | return { code: 404, message: '所选分类不存在!' }; 175 | } 176 | const array = await this.getAllClassifyIds(classifyId); 177 | const ids = await this.artRepository.createQueryBuilder('art') 178 | .where('"art"."classifyId" in(:...id)', { 179 | id: array 180 | }) 181 | .getMany(); 182 | if (!ids.length) { 183 | return { code: 404, message: '原分类下不存在文章!' }; 184 | } 185 | try { 186 | // 修改文章分类 187 | for (const i of ids) { 188 | await this.artRepository.update(i.id, { classify: newClassify }); 189 | } 190 | } catch (err) { 191 | throw new HttpException(err.toString(), 500); 192 | } 193 | } 194 | 195 | } -------------------------------------------------------------------------------- /src/cms/services/index.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, HttpException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository, TreeRepository } from 'typeorm'; 4 | import { Index } from '../entities/index.entity'; 5 | const _ = require('underscore'); 6 | 7 | 8 | @Injectable() 9 | export class IndexService { 10 | 11 | constructor( 12 | @InjectRepository(Index) private readonly indexRepo: Repository 13 | ) { } 14 | 15 | async getIndex() { 16 | const data = await this.indexRepo.findOne(); 17 | return data; 18 | } 19 | 20 | async updateIndex(index: Index) { 21 | try { 22 | index.id = 1; 23 | await this.indexRepo.save(this.indexRepo.create(index)); 24 | } catch (error) { 25 | throw new HttpException(error.toString(), 500); 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /src/cms/services/page-sort.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { PageSort } from '../entities/page-sort.entity'; 4 | import { TreeRepository, In } from 'typeorm'; 5 | import { Page } from '../entities/page.entity'; 6 | import { CreatePageSort } from '../interfaces/page-sort.interface'; 7 | 8 | @Injectable() 9 | export class PageSortService { 10 | constructor( 11 | @InjectRepository(PageSort) private readonly psRepo: TreeRepository, 12 | @InjectRepository(Page) private readonly pageRepo: TreeRepository, 13 | ) { } 14 | 15 | async createPageSort(pageSort: CreatePageSort) { 16 | try { 17 | const ignore = await this.psRepo.count(); 18 | if (!pageSort.parent.value || ignore <= 0) { 19 | await this.psRepo.save({ label: pageSort.label, value: pageSort.value }); 20 | return { code: 200, message: '创建成功' }; 21 | } 22 | if (pageSort.parent) { 23 | const exist = await this.psRepo.findOne({ where: { value: pageSort.parent.value } }); 24 | if (!exist) { 25 | throw new HttpException('当前分类父节点不存在!', 404); 26 | } 27 | pageSort.parent = exist; 28 | } 29 | const result = await this.psRepo.findOne({ where: { value: pageSort.value } }); 30 | if (result) { 31 | throw new HttpException('别名重复!', 406); 32 | } 33 | await this.psRepo.save(this.psRepo.create(pageSort)); 34 | } catch (error) { 35 | throw new HttpException(error.toString(), 400); 36 | } 37 | } 38 | 39 | async updatePageSort(pageSort: PageSort) { 40 | const exist = await this.psRepo.findOne(pageSort.id, { relations: ['parent'] }); 41 | if (!exist) { 42 | throw new HttpException('该页面分类不存在!', 404); 43 | } 44 | if (pageSort.value && pageSort.value !== exist.value) { 45 | if (await this.psRepo.findOne({ value: pageSort.value })) { 46 | throw new HttpException('别名重复!', 406); 47 | } 48 | } 49 | const parent = await this.psRepo.findOne({ where: { value: pageSort.parent.value } }); 50 | if (!parent) { 51 | throw new HttpException('该上级分类不存在!', 404); 52 | } 53 | try { 54 | pageSort.parent = parent; 55 | await this.psRepo.save(await this.psRepo.create(pageSort)); 56 | } catch (error) { 57 | throw new HttpException(error.toString(), 400); 58 | } 59 | } 60 | 61 | async deletePageSort(id: number) { 62 | const pageSort = await this.psRepo.findOne(id); 63 | if (!pageSort) { 64 | throw new HttpException('该页面分类不存在!', 404); 65 | } 66 | const array = await this.getAllClassifyIds(id); 67 | const pages = await this.pageRepo.count({ where: { pageSort: In(array) } }); 68 | if (pages > 0) { 69 | throw new HttpException('当前分类下有页面,不能删除', 403); 70 | } 71 | array.splice(array.indexOf(id), 1); 72 | if (array.length) { 73 | throw new HttpException('当前分类下有子分类,不能删除', 403); 74 | } 75 | await this.psRepo.remove(pageSort); 76 | } 77 | 78 | async getAllPageSort() { 79 | return await this.psRepo.findTrees(); 80 | } 81 | 82 | async getOnePageSort(alias: string) { 83 | const exist = await this.psRepo.findOne({ where: { value: alias } }); 84 | if (!exist) { 85 | throw new HttpException('该页面分类不存在!', 404); 86 | } 87 | const data = await this.psRepo.findDescendantsTree(exist); 88 | return data; 89 | } 90 | 91 | async getAllClassifyIds(idNum: number): Promise { 92 | const array: number[] = []; 93 | const classify = await this.psRepo.findOne({ id: idNum }); 94 | await this.psRepo.findDescendants(classify).then(a => { 95 | if (a) { 96 | a.map(a => { 97 | array.push(a.id); 98 | }); 99 | } 100 | }); 101 | return array; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/cms/services/page.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Page } from '../entities/page.entity'; 4 | import { Repository, Like } from 'typeorm'; 5 | import { Content } from '../entities/content.entity'; 6 | import * as moment from 'moment'; 7 | import { PageSort } from '../entities/page-sort.entity'; 8 | import { PageInput, PageUpdateInput } from '../interfaces/page.interface'; 9 | 10 | @Injectable() 11 | export class PageService { 12 | constructor( 13 | @InjectRepository(Page) private readonly pageRepo: Repository, 14 | @InjectRepository(PageSort) private readonly psRepo: Repository, 15 | @InjectRepository(Content) private readonly contentRepo: Repository, 16 | ) { } 17 | 18 | async createPage(page: PageInput) { 19 | const ps = await this.psRepo.findOne({ where: { value: page.pageSortAlias } }); 20 | if (!ps) { 21 | throw new HttpException('该页面分类不存在!', 404); 22 | } 23 | const exist = await this.pageRepo.findOne({ where: { alias: page.alias } }); 24 | if (exist) { 25 | throw new HttpException('别名重复!', 406); 26 | } 27 | const time = moment().format('YYYY-MM-DD HH:mm:ss'); 28 | try { 29 | const result = await this.pageRepo.save(this.pageRepo.create({ 30 | name: page.name, 31 | alias: page.alias, 32 | lastUpdateTime: time, 33 | pageSort: ps, 34 | structure: page.structure 35 | })); 36 | for (const i of page.contents) { 37 | await this.contentRepo.save(this.contentRepo.create({ 38 | name: i.name, 39 | alias: i.alias, 40 | value: i.value, 41 | page: result 42 | })); 43 | } 44 | } catch (error) { 45 | throw new HttpException(error.toString(), 400); 46 | } 47 | } 48 | 49 | async updatePage(page: PageUpdateInput) { 50 | const exist = await this.pageRepo.findOne(page.id, { relations: ['contents'] }); 51 | const pageSort = await this.psRepo.findOne({ where: { value: page.pageSortAlias } }); 52 | if (!pageSort) { 53 | throw new HttpException('该页面分类不存在!', 404); 54 | } 55 | if (page.alias !== exist.alias) { 56 | if (await this.pageRepo.findOne({ where: { alias: page.alias, pageSort } })) { 57 | throw new HttpException('页面别名重复!', 406); 58 | } 59 | } 60 | const same = await this.contentRepo.find({ where: { page: page.id } }); 61 | await this.contentRepo.remove(same); 62 | await this.pageRepo.save(this.pageRepo.create(page)); 63 | } 64 | 65 | async deletePage(alias: [string]) { 66 | const pages: number[] = []; 67 | for (const i of alias) { 68 | const a = await this.pageRepo.findOne({ where: { alias: i } }); 69 | if (!a) { 70 | throw new HttpException(`${alias}页面不存在`, 404); 71 | } 72 | pages.push(a.id); 73 | } 74 | await this.pageRepo.delete(pages); 75 | } 76 | 77 | async getAllPage(name: string, alias: string, pageNumber: number, pageSize: number) { 78 | const sqb = this.pageRepo.createQueryBuilder('page') 79 | .leftJoinAndSelect('page.contents', 'contents') 80 | .leftJoinAndSelect('page.pageSort', 'pageSort'); 81 | if (name) { 82 | sqb.andWhere('page.name Like :name', { name: `%${name}%` }); 83 | } 84 | if (alias) { 85 | sqb.andWhere('page.alias = :alias', { alias }); 86 | } 87 | const result = await sqb.skip(pageSize * (pageNumber - 1)).take(pageSize).orderBy({ 'page.lastUpdateTime': 'DESC' }).getManyAndCount(); 88 | return { data: result[0], total: result[1] }; 89 | } 90 | 91 | async getOnePage(alias: string) { 92 | const result = await this.pageRepo.findOne({ where: { alias }, relations: ['contents', 'pageSort'] }); 93 | const a = { 94 | id: result.id, 95 | name: result.name, 96 | alias: result.alias, 97 | lastUpdateTime: result.lastUpdateTime, 98 | contents: result.contents.length ? result.contents.map(item => { 99 | return { 100 | name: item.name, 101 | alias: item.alias, 102 | value: item.value 103 | }; 104 | }) : [], 105 | pageSortValue: result.pageSort.value, 106 | structure: result.structure 107 | }; 108 | return a; 109 | } 110 | 111 | } -------------------------------------------------------------------------------- /src/cms/services/pic-group.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, HttpException } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { PictureGroup } from '../entities/picture-group.entity'; 4 | import { Repository } from 'typeorm'; 5 | import { Picture } from '../entities/picture.entity'; 6 | 7 | @Injectable() 8 | export class PicGroupService { 9 | constructor( 10 | @InjectRepository(PictureGroup) 11 | private readonly pGRepo: Repository, 12 | @InjectRepository(Picture) 13 | private readonly picRepo: Repository, 14 | ) { } 15 | 16 | async findpGs(currentPage: number, pageSize: number) { 17 | const data = await this.pGRepo.find({ 18 | relations: ['pictures'], 19 | order: { id: 'ASC' }, 20 | skip: (currentPage - 1) * pageSize, 21 | take: pageSize, 22 | }); 23 | const total = await this.pGRepo.count(); 24 | return { data, total }; 25 | } 26 | 27 | async findOne(id: number) { 28 | const data = await this.pGRepo.findOne({ 29 | where: { id }, 30 | relations: ['pictures'], 31 | }); 32 | return data; 33 | } 34 | 35 | async addPicture(id: number, pic: Picture) { 36 | const picGroup = await this.pGRepo.create(await this.pGRepo.findOne(id)); 37 | await this.picRepo.save({ 38 | name: pic.name, 39 | title: pic.title, 40 | Url: pic.Url, 41 | sequence: pic.sequence, 42 | pictureGroup: picGroup 43 | }); 44 | return 'success'; 45 | } 46 | 47 | async updatePicture(pic: Picture) { 48 | await this.picRepo.save(await this.picRepo.create(pic)); 49 | return 'success'; 50 | } 51 | 52 | async delPicture(id: number) { 53 | await this.picRepo.remove(await this.picRepo.findOne(id)); 54 | return 'success'; 55 | } 56 | 57 | async delPicGroup(id: number) { 58 | await this.pGRepo.remove(await this.pGRepo.findOne(id)); 59 | return 'success'; 60 | } 61 | 62 | async addPicGroup(name: string) { 63 | 64 | const exist = await this.pGRepo.findOne({ where: { name } }); 65 | if (exist) { 66 | throw new HttpException('该图组已存在', 409); 67 | } 68 | return await this.pGRepo.save({ name }); 69 | } 70 | 71 | async updatePicGroup(pG: PictureGroup) { 72 | await this.pGRepo.save(pG); 73 | } 74 | 75 | async findPicture(id: number) { 76 | const data = await this.picRepo.findOne(id); 77 | if (!data) { 78 | throw new HttpException('该图片不存在!', 404); 79 | } 80 | return data; 81 | } 82 | 83 | 84 | } -------------------------------------------------------------------------------- /src/cms/services/user-message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | import { UserMessage } from '../entities/user-message.entity'; 5 | 6 | @Injectable() 7 | export class UserMessageService { 8 | constructor( 9 | @InjectRepository(UserMessage) 10 | private readonly adRepo: Repository, 11 | ) { } 12 | 13 | async findNoread(id: number) { 14 | let data = false; 15 | const exist = await this.adRepo.find({ 16 | where: { owner: id, state: false } 17 | }); 18 | if (exist && exist.length) { 19 | data = true; 20 | } 21 | return data; 22 | } 23 | 24 | async findAll(id: number, pageNumber: number, pageSize: number) { 25 | const data = await this.adRepo.find({ 26 | where: { owner: id }, 27 | order: { state: 'ASC', createdAt: 'DESC' }, 28 | skip: (pageNumber - 1) * pageSize, 29 | take: pageSize, 30 | }); 31 | return { data: data[0], total: data[1] }; 32 | } 33 | 34 | async findOne(id: number) { 35 | const data = await this.adRepo.findOne({ id }); 36 | return data; 37 | } 38 | 39 | async readOne(id: number) { 40 | const news = await this.adRepo.findOne({ id }); 41 | news.state = true; 42 | await this.adRepo.save(this.adRepo.create(news)); 43 | } 44 | 45 | async readAll(owner: number) { 46 | await this.adRepo.createQueryBuilder().update(UserMessage).set({ state: true }).where({ owner }).execute(); 47 | } 48 | 49 | async del(id: number) { 50 | const news = await this.adRepo.findOne({ id }); 51 | return this.adRepo.remove(this.adRepo.create(news)); 52 | } 53 | 54 | async dels(ids: [number]) { 55 | const exist = await this.adRepo.findByIds(ids); 56 | return this.adRepo.remove(exist); 57 | } 58 | } -------------------------------------------------------------------------------- /src/interceptors/errors.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, HttpException, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { catchError } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export class ErrorsInterceptor implements NestInterceptor { 7 | intercept(context: ExecutionContext, calls$: Observable): Observable { 8 | return calls$.pipe(catchError((error, caught) => { 9 | if (error instanceof HttpException) { 10 | return Promise.resolve({ 11 | code: error.getStatus(), 12 | message: error.getResponse() 13 | }); 14 | } else { 15 | return Promise.resolve({ 16 | code: 500, 17 | message: '出现了意外错误:' + error.toString() 18 | }); 19 | } 20 | })); 21 | } 22 | } -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "*.ts", 5 | "**/*.ts" 6 | ], 7 | "exclude": [ 8 | "./**/*.spec.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/user/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Inject, Injectable } from '@nestjs/common'; 2 | import { AuthenticationError } from 'apollo-server-core'; 3 | import { __ as t } from 'i18n'; 4 | import * as jwt from 'jsonwebtoken'; 5 | 6 | import { AUTH_TOKEN_WHITE_LIST } from '../constants/auth.constant'; 7 | import { User } from '../entities/user.entity'; 8 | import { JwtPayload, JwtReply } from '../interfaces/jwt.interface'; 9 | import { UserService } from '../services/user.service'; 10 | 11 | @Injectable() 12 | export class AuthService { 13 | constructor( 14 | @Inject(forwardRef(() => UserService)) private readonly userService: UserService, 15 | @Inject(AUTH_TOKEN_WHITE_LIST) private readonly authTokenWhiteList: [string], 16 | ) { } 17 | 18 | async createToken(payload: JwtPayload): Promise { 19 | const accessToken = jwt.sign(payload, 'secretKey', { expiresIn: '1d' }); 20 | return { accessToken, expiresIn: 60 * 60 * 24 }; 21 | } 22 | 23 | async validateUser(req: any): Promise { 24 | 25 | /** 26 | * whitelist 27 | */ 28 | // tslint:disable-next-line:max-line-length 29 | if (req.body && ([ 30 | 'IntrospectionQuery', 'sayHello', 'login', 'getOnePage', 31 | 'adminLogin', 'register', 'getAllArticle', 32 | 'userGetArticles', 'getAllClassify', 'getArticleById', 'uploadProcess', 33 | 34 | ].some(item => req.body.query.includes(item)))) { 35 | return; 36 | } 37 | 38 | let token = req.headers.authorization as string; 39 | if (!token) { 40 | throw new AuthenticationError(t('Request header lacks authorization parameters,it should be: Authorization')); 41 | } 42 | 43 | if (token.slice(0, 6) === 'Bearer') { 44 | token = token.slice(7); 45 | } else { 46 | throw new AuthenticationError(t('The authorization code prefix is incorrect. it should be: Bearer')); 47 | } 48 | 49 | try { 50 | const decodedToken = <{ loginName: string }>jwt.verify(token, 'secretKey'); 51 | const data = await this.userService.findOneWithRoles(decodedToken.loginName); 52 | return data; 53 | } catch (error) { 54 | if (error instanceof jwt.JsonWebTokenError) { 55 | throw new AuthenticationError(t('The authorization code is incorrect')); 56 | } 57 | if (error instanceof jwt.TokenExpiredError) { 58 | throw new AuthenticationError(t('The authorization code has expired')); 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/user/constants/auth.constant.ts: -------------------------------------------------------------------------------- 1 | export const AUTH_TOKEN_WHITE_LIST = 'AUTH_TOKEN_WHITE_LIST'; -------------------------------------------------------------------------------- /src/user/entities/role.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { User } from './user.entity'; 4 | 5 | @Entity('role') 6 | export class Role { 7 | @PrimaryGeneratedColumn() 8 | id: number; 9 | 10 | @Column({ 11 | unique: true 12 | }) 13 | name: string; 14 | 15 | @ManyToMany(type => User, user => user.roles) 16 | users: User[]; 17 | 18 | } -------------------------------------------------------------------------------- /src/user/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import { 3 | Column, 4 | CreateDateColumn, 5 | Entity, 6 | JoinTable, 7 | ManyToMany, 8 | OneToMany, 9 | PrimaryGeneratedColumn, 10 | UpdateDateColumn, 11 | } from 'typeorm'; 12 | 13 | import { Role } from './role.entity'; 14 | 15 | @Entity('user') 16 | export class User { 17 | @PrimaryGeneratedColumn() 18 | id: number; 19 | 20 | @Column({ 21 | nullable: true, 22 | unique: true 23 | }) 24 | username: string; 25 | 26 | @Column({ 27 | unique: true, 28 | nullable: true 29 | }) 30 | email: string; 31 | 32 | @Column({ 33 | unique: true, 34 | nullable: true 35 | }) 36 | mobile: string; 37 | 38 | @Column() 39 | password: string; 40 | 41 | @Column({ 42 | nullable: true 43 | }) 44 | avater: string; 45 | 46 | @Column({ 47 | nullable: true 48 | }) 49 | nickname: string; 50 | 51 | @Column({ 52 | default: false 53 | }) 54 | banned: boolean; 55 | 56 | @Column({ 57 | default: false 58 | }) 59 | recycle: boolean; 60 | 61 | @ManyToMany(type => Role, role => role.users, { 62 | onDelete: 'CASCADE' 63 | }) 64 | @JoinTable() 65 | roles: Role[]; 66 | 67 | @CreateDateColumn({ 68 | transformer: { 69 | from: (date: Date) => { 70 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 71 | }, 72 | to: () => { 73 | return new Date(); 74 | } 75 | } 76 | }) 77 | createTime: string; 78 | 79 | @UpdateDateColumn({ 80 | transformer: { 81 | from: (date: Date) => { 82 | return moment(date).format('YYYY-MM-DD HH:mm:ss'); 83 | }, 84 | to: () => { 85 | return new Date(); 86 | } 87 | } 88 | }) 89 | updateTime: string; 90 | } -------------------------------------------------------------------------------- /src/user/graphqls/common.types.graphql: -------------------------------------------------------------------------------- 1 | "User module universal response body" 2 | type UserModuleCommonResult { 3 | "status code" 4 | code: Int 5 | "response message" 6 | message: String 7 | } 8 | 9 | "JSON data" 10 | scalar JSON -------------------------------------------------------------------------------- /src/user/graphqls/role.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | "Query all roles" 3 | findRoles: RolesResult 4 | } 5 | 6 | type Mutation { 7 | "Create a role" 8 | createRole(name: String!): UserModuleCommonResult 9 | "Update the specified role's name" 10 | updateRole(id: Int!, name: String!): UserModuleCommonResult 11 | "Delete the specified role" 12 | deleteRole(id: Int!): UserModuleCommonResult 13 | } 14 | 15 | type RolesResult { 16 | code: Int 17 | message: String 18 | data: [Role] 19 | } 20 | 21 | type Role { 22 | id: Int 23 | name: String 24 | } 25 | -------------------------------------------------------------------------------- /src/user/graphqls/user.types.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | "Ordinary user login" 3 | login(username: String!, password: String!): LoginResponse 4 | "Admin login" 5 | adminLogin(username: String!, password: String!): LoginResponse 6 | "Query the currently logged in user information" 7 | findCurrentUserInfo: UserInfoResult 8 | "Query user information by ID array" 9 | findUserInfoByIds(userIds: [Int]!): UsersInfoResult 10 | "Query all user information under the specified role ID" 11 | findUsersInRole(roleId: Int!): UsersInfoResult 12 | "Query all user information under the specified organization ID" 13 | findUsersInOrganization(organizationId: Int!): UsersInfoResult 14 | "验证用户密码" 15 | checkPwd(username: String!,password:String!): checkPwdResult 16 | } 17 | 18 | type Mutation { 19 | "Ordinary user registration" 20 | register(registerUserInput: RegisterUserInput): UserModuleCommonResult 21 | "Create user" 22 | createUser(createUserInput: CreateUserInput): UserModuleCommonResult 23 | "Add a role to the user" 24 | addUserRole(userId: Int!, roleId: Int!): UserModuleCommonResult 25 | "Ban user" 26 | banUser(userId: Int!): UserModuleCommonResult 27 | "Delete a role from the user" 28 | deleteUserRole(userId: Int!, roleId: Int!): UserModuleCommonResult 29 | "Delete user to recycle bin" 30 | recycleUser(userId: Int!): UserModuleCommonResult 31 | "Delete user in the recycle bin" 32 | deleteRecycledUser(userId: [Int]!): UserModuleCommonResult 33 | "Revert banned user" 34 | revertBannedUser(userId: Int!): UserModuleCommonResult 35 | "Revert recycled user" 36 | revertRecycledUser(userId: Int!): UserModuleCommonResult 37 | "Specify user information by ID update" 38 | updateUserInfoById(userId: Int!, updateUserInput: UpdateUserInput): UserModuleCommonResult 39 | "Update current login user information" 40 | updateCurrentUserInfo(updateCurrentUserInput: UpdateCurrentUserInput): UserModuleCommonResult 41 | "Find password" 42 | findPassword(mobile: String!,password: String!): UserModuleCommonResult 43 | } 44 | 45 | type checkPwdResult{ 46 | code: Int 47 | message: String 48 | data: Boolean 49 | } 50 | 51 | input RegisterUserInput { 52 | username: String! 53 | email: String 54 | mobile: String 55 | password: String! 56 | nickname: String 57 | avater: String 58 | } 59 | 60 | input CreateUserInput { 61 | username: String! 62 | email: String 63 | mobile: String 64 | password: String! 65 | roleIds: [Int] 66 | nickname: String 67 | avater: String 68 | } 69 | 70 | input UpdateUserInput { 71 | username: String 72 | email: String 73 | mobile: String 74 | password: String 75 | avater: String 76 | nickname: String 77 | "An array of role ID objects owned, each item must contain the role ID (before) before the update, and the role ID (after) after the update" 78 | roleIds: [RoleOrOrganizationIdData] 79 | } 80 | 81 | input UpdateCurrentUserInput { 82 | email: String 83 | mobile: String 84 | password: String 85 | nickname: String 86 | avater: String 87 | } 88 | 89 | input RegisterOrCreateInfoKV { 90 | "Information item ID" 91 | key: Int 92 | "Information item value" 93 | value: String 94 | } 95 | 96 | input UpdateInfoKV { 97 | "ID of the item value(userInfo.id)" 98 | key: Int 99 | "Information item value(userInfo.value)" 100 | value: String 101 | "Information item ID(infoItem.id)" 102 | relationId: Int 103 | } 104 | 105 | input RoleOrOrganizationIdData { 106 | "Update previous ID" 107 | before: Int 108 | "Updated ID" 109 | after: Int 110 | } 111 | 112 | type LoginResponse { 113 | code: Int 114 | message: String 115 | data: AccessToken 116 | } 117 | 118 | type AccessToken { 119 | accessToken: String 120 | expiresIn: Int 121 | } 122 | 123 | type UserInfoResult { 124 | code: Int 125 | message: String 126 | data: UserInfoData 127 | } 128 | 129 | type UsersInfoResult { 130 | code: Int 131 | message: String 132 | data: [UserInfoData] 133 | } 134 | 135 | type UserInfoData { 136 | id:Int 137 | username: String 138 | email: String 139 | mobile: String 140 | banned: Boolean 141 | recycle: Boolean 142 | createTime: String 143 | updateTime: String 144 | userRoles: [Role] 145 | nickname: String 146 | avater: String 147 | } 148 | 149 | type UserInfo { 150 | id:Int 151 | order: Int 152 | "Information item ID(infoItem.id)" 153 | relationId: Int 154 | type: String 155 | name: String 156 | value: String 157 | description: String 158 | registerDisplay: Boolean 159 | informationDisplay: Boolean 160 | } 161 | -------------------------------------------------------------------------------- /src/user/i18n/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization_manage": "organization_manage", 3 | "user_manage": "user_manage", 4 | "role_manage": "role_manage", 5 | "resource_manage": "resource_manage", 6 | "system_module_manage": "system_module_manage", 7 | "info_group_manage": "info_group_manage", 8 | "info_item_manage": "info_item_manage", 9 | "find_root_organizations": "find_root_organizations", 10 | "find_all_organizations": "find_all_organizations", 11 | "find_children_organizations": "find_children_organizations", 12 | "create_organization": "create_organization", 13 | "update_organization": "update_organization", 14 | "delete_organization": "delete_organization", 15 | "add_users_to_organization": "add_users_to_organization", 16 | "delete_user_from_organization": "delete_user_from_organization", 17 | "create_user": "create_user", 18 | "add_user_role": "add_user_role", 19 | "delete_user_role": "delete_user_role", 20 | "recycle_user": "recycle_user", 21 | "delete_recycled_user": "delete_recycled_user", 22 | "update_user_info_by_id": "update_user_info_by_id", 23 | "find_user_info_by_ids": "find_user_info_by_ids", 24 | "find_users_in_role": "find_users_in_role", 25 | "find_users_in_organization": "find_users_in_organization", 26 | "create_role": "create_role", 27 | "delete_role": "delete_role", 28 | "update_role": "update_role", 29 | "set_permissions_to_role": "set_permissions_to_role", 30 | "find_roles": "find_roles", 31 | "find_one_role_info": "find_one_role_info", 32 | "find_resources": "find_resources", 33 | "find_system_modules": "find_system_modules", 34 | "create_info_group": "create_info_group", 35 | "delete_info_group": "delete_info_group", 36 | "update_info_group": "update_info_group", 37 | "add_info_item_to_info_group": "add_info_item_to_info_group", 38 | "delete_into_item_from_info_group": "delete_into_item_from_info_group", 39 | "find_all_info_group": "find_all_info_group", 40 | "find_info_items_by_group_id": "find_info_items_by_group_id", 41 | "create_info_item": "create_info_item", 42 | "delete_info_item": "delete_info_item", 43 | "update_info_item": "update_info_item", 44 | "find_all_info_item": "find_all_info_item", 45 | "Get the root organization successfully": "Get the root organization successfully", 46 | "Get all organizations successful": "Get all organizations successful", 47 | "Get all sub-organizations below the organization successfully": "Get all sub-organizations below the organization successfully", 48 | "Create an organizational successfully": "Create an organizational successfully", 49 | "Update organization successfully": "Update organization successfully", 50 | "Delete organization successfully": "Delete organization successfully", 51 | "Add users to your organization successfully": "Add users to your organization successfully", 52 | "Delete users from your organization successfully": "Delete users from your organization successfully", 53 | "Login success": "Login success", 54 | "Registration success": "Registration success", 55 | "Create user successfully": "Create user successfully", 56 | "Add user role successfully": "Add user role successfully", 57 | "Delete user role successfully": "Delete user role successfully", 58 | "Delete user to recycle bin successfully": "Delete user to recycle bin successfully", 59 | "Delete user in the recycle bin successfully": "Delete user in the recycle bin successfully", 60 | "Update user information successfully": "Update user information successfully", 61 | "Update current login user information successfully": "Update current login user information successfully", 62 | "Query the specified users information successfully": "Query the specified users information successfully", 63 | "Query the current login user information successfully": "Query the current login user information successfully", 64 | "Query user registration information item successfully": "Query user registration information item successfully", 65 | "Query the user under the role successfully": "Query the user under the role successfully", 66 | "Query users under the organization successfully": "Query users under the organization successfully", 67 | "Create a role successfully": "Create a role successfully", 68 | "Delete role successfully": "Delete role successfully", 69 | "Update role successfully": "Update role successfully", 70 | "Set role permissions successfully": "Set role permissions successfully", 71 | "Query all roles successfully": "Query all roles successfully", 72 | "Query role information successfully": "Query role information successfully", 73 | "Query the resource successfully": "Query the resource successfully", 74 | "Query the system modules successfully": "Query the system modules successfully", 75 | "Create a information group successfully": "Deleted the information group successfully", 76 | "Deleted the information group successfully": "Deleted the information group successfully", 77 | "Update the information group successfully": "Update the information group successfully", 78 | "Add an information item to the information group successfully": "Add an information item to the information group successfully", 79 | "Delete the information item in the information group successfully": "Delete the information item in the information group successfully", 80 | "Query all information groups successfully": "Query all information groups successfully", 81 | "Query the information item in the information group successfully": "Query the information item in the information group successfully", 82 | "Create information item successfully": "Create information item successfully", 83 | "Delete information item successfully": "Delete information item successfully", 84 | "Update information item successfully": "Update information item successfully", 85 | "Query all information items successfully": "Query all information items successfully", 86 | "Name already exists": "Name already exists", 87 | "Database error %s": "Database error %s", 88 | "The role information group already exists": "The role information group already exists", 89 | "Information item with id [%s] already exists": "Information item with id [%s] already exists", 90 | "The organization with id of %s does not exist": "The organization with id of %s does not exist", 91 | "The parent organization with id of %s does not exist": "The parent organization with id of %s does not exist", 92 | "Cannot delete the organization that have child organizations": "Cannot delete the organization that have child organizations", 93 | "The user id of %s does not exist": "The user id of %s does not exist", 94 | "User with id of %s is already under organization": "User with id of %s is already under organization", 95 | "The user id of %s does not appear in this organization": "The user id of %s does not appear in this organization", 96 | "The role id of %s does not exist": "The role id of %s does not exist", 97 | "The permission id of %s does not exist": "The permission id of %s does not exist", 98 | "No users belong to this role": "No users belong to this role", 99 | "No users belong to this organization": "No users belong to this Organization", 100 | "Please make sure the username, mobile phone number, and email exist at least one": "Please make sure the username, mobile phone number, and email exist at least one", 101 | "User does not exist": "User does not exist", 102 | "User is banned": "User is banned", 103 | "invalid password": "invalid password", 104 | "Username already exists": "Username already exists", 105 | "Mobile already exists": "Mobile already exists", 106 | "Email already exists": "Email already exists", 107 | "Request header lacks authorization parameters,it should be: Authorization or authorization": "Request header lacks authorization parameters,it should be: Authorization or authorization", 108 | "The authorization code prefix is incorrect. it should be: Bearer or bearer": "The authorization code prefix is incorrect. it should be: Bearer or bearer", 109 | "The authorization code is incorrect": "The authorization code is incorrect", 110 | "The authorization code has expired": "The authorization code has expired", 111 | "ordinary user": "ordinary user", 112 | "ordinary user information group": "ordinary user information group", 113 | "Ban user successfully": "Ban user successfully", 114 | "Revert banned user successfully": "Revert banned user successfully", 115 | "Revert recycled user successfully": "Revert recycled user successfully", 116 | "You are not authorized to access": "You are not authorized to access", 117 | "ban_user": "ban_user", 118 | "revert_banned_user": "revert_banned_user", 119 | "revert_recycled_user": "revert_recycled_user", 120 | "UserModule": "UserModule" 121 | } -------------------------------------------------------------------------------- /src/user/i18n/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "organization_manage": "组织管理", 3 | "user_manage": "用户管理", 4 | "role_manage": "角色管理", 5 | "resource_manage": "资源管理", 6 | "system_module_manage": "系统模块管理", 7 | "info_group_manage": "信息组管理", 8 | "info_item_manage": "信息项管理", 9 | "find_root_organizations": "查询根组织", 10 | "find_all_organizations": "查询所有组织", 11 | "find_children_organizations": "查询组织下的子组织", 12 | "create_organization": "创建组织", 13 | "update_organization": "更新组织", 14 | "delete_organization": "删除组织", 15 | "add_users_to_organization": "向组织添加用户", 16 | "delete_user_from_organization": "删除组织中的用户", 17 | "create_user": "创建用户", 18 | "add_user_role": "添加用户角色", 19 | "delete_user_role": "删除用户角色", 20 | "recycle_user": "删除用户到回收站", 21 | "delete_recycled_user": "删除回收站内的用户", 22 | "update_user_info_by_id": "更新指定用户信息", 23 | "find_user_info_by_ids": "查询指定用户信息", 24 | "find_users_in_role": "查询角色下的用户", 25 | "find_users_in_organization": "查询组织下的用户", 26 | "create_role": "创建角色", 27 | "delete_role": "删除角色", 28 | "update_role": "更新角色", 29 | "set_permissions_to_role": "设置角色权限", 30 | "find_roles": "查询所有角色", 31 | "find_one_role_info": "查询角色信息", 32 | "find_resources": "查询资源", 33 | "find_system_modules": "查询系统模块信息", 34 | "create_info_group": "创建信息组", 35 | "delete_info_group": "删除信息组", 36 | "update_info_group": "更新信息组", 37 | "add_info_item_to_info_group": "向信息组添加信息项", 38 | "delete_into_item_from_info_group": "删除信息组中的信息项", 39 | "find_all_info_group": "查询所有信息组", 40 | "find_info_items_by_group_id": "查询信息组中的信息项", 41 | "create_info_item": "创建信息项", 42 | "delete_info_item": "删除信息项", 43 | "update_info_item": "更新信息项", 44 | "find_all_info_item": "查询所有信息项", 45 | "Get the root organization successfully": "获取根组织成功", 46 | "Get all organizations successful": "获取所有组织成功", 47 | "Get all sub-organizations below the organization successfully": "获取组织下面的所有子组织成功", 48 | "Create an organizational successfully": "创建组织成功", 49 | "Update organization successfully": "更新组织成功", 50 | "Delete organization successfully": "删除组织成功", 51 | "Add users to your organization successfully": "向组织添加用户成功", 52 | "Delete users from your organization successfully": "删除组织中的用户成功", 53 | "Login success": "登录成功", 54 | "Registration success": "注册成功", 55 | "Create user successfully": "创建用户成功", 56 | "Add user role successfully": "添加用户角色成功", 57 | "Delete user role successfully": "删除用户角色成功", 58 | "Delete user to recycle bin successfully": "删除用户到回收站成功", 59 | "Delete user in the recycle bin successfully": "删除回收站内的用户成功", 60 | "Update user information successfully": "更新用户信息成功", 61 | "Update current login user information successfully": "更新当前登录用户信息成功", 62 | "Query the specified users information successfully": "查询指定用户信息成功", 63 | "Query the current login user information successfully": "查询当前登录用户信息成功", 64 | "Query user registration information item successfully": "查询用户注册信息项成功", 65 | "Query the user under the role successfully": "获取角色下的用户成功", 66 | "Query users under the organization successfully": "获取组织下的用户成功", 67 | "Create a role successfully": "创建角色成功", 68 | "Delete role successfully": "删除角色成功", 69 | "Update role successfully": "更新角色成功", 70 | "Set role permissions successfully": "设置角色权限成功", 71 | "Query all roles successfully": "查询所有角色成功", 72 | "Query role information successfully": "查询角色信息成功", 73 | "Query the resource successfully": "获取资源成功", 74 | "Query the system modules successfully": "查询系统模块信息成功", 75 | "Create a information group successfully": "创建信息组成功", 76 | "Deleted the information group successfully": "删除信息组成功", 77 | "Update the information group successfully": "更新信息组成功", 78 | "Add an information item to the information group successfully": "向信息组添加信息项成功", 79 | "Delete the information item in the information group successfully": "删除信息组中的信息项成功", 80 | "Query all information groups successfully": "查询所有信息组成功", 81 | "Query the information item in the information group successfully": "查询信息项成功", 82 | "Create information item successfully": "创建信息项成功", 83 | "Delete information item successfully": "删除信息项成功", 84 | "Update information item successfully": "更新信息项成功", 85 | "Query all information items successfully": "查询所有信息项成功", 86 | "Database error %s": "数据库错误,%s", 87 | "Name already exists": "名称已存在", 88 | "The role information group already exists": "该角色信息组已存在", 89 | "Information item with id [%s] already exists": "ID 为 [%s] 的信息项已存在", 90 | "The organization with id of %s does not exist": "ID 为 %s 的组织不存在", 91 | "The parent organization with id of %s does not exist": "ID 为 %s 的父组织不存在", 92 | "Cannot delete the organization that have child organizations": "不能删除存在子组织的组织", 93 | "The user id of %s does not exist": "ID 为 %s 的用户不存在", 94 | "User with id of %s is already under organization": "ID 为 %s 的用户已在该组织中", 95 | "The user id of %s does not appear in this organization": "ID 为 %s 的用户不在该组织中", 96 | "The role id of %s does not exist": "ID 为 %s 的角色不存在", 97 | "The permission id of %s does not exist": "ID 为 %s 的权限不存在", 98 | "No users belong to this role": "没有用户属于这个角色", 99 | "No users belong to this organization": "没有用户属于这个组织", 100 | "Please make sure the username, mobile phone number, and email exist at least one": "请确保用户名,手机号码和电子邮件至少存在一个", 101 | "User does not exist": "用户不存在", 102 | "User is banned": "用户被封禁", 103 | "invalid password": "密码错误", 104 | "Username already exists": "用户名已存在", 105 | "Mobile already exists": "手机号已存在", 106 | "Email already exists": "邮箱已存在", 107 | "Request header lacks authorization parameters,it should be: Authorization or authorization": "请求头缺少授权参数,授权参数名称为 Authorization 或 authorization", 108 | "The authorization code prefix is incorrect. it should be: Bearer or bearer": "授权码前缀有误,前缀为 Bearer 或 bearer", 109 | "The authorization code is incorrect": "授权码错误", 110 | "The authorization code has expired": "授权码已过期", 111 | "ordinary user": "普通用户", 112 | "ordinary user information group": "普通用户信息组", 113 | "Ban user successfully": "封禁用户成功", 114 | "Revert banned user successfully": "解封用户成功", 115 | "Revert recycled user successfully": "恢复用户成功", 116 | "You are not authorized to access": "您无权访问", 117 | "ban_user": "封禁用户", 118 | "revert_banned_user": "解封用户", 119 | "revert_recycled_user": "恢复用户", 120 | "UserModule": "用户模块" 121 | } -------------------------------------------------------------------------------- /src/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities/user.entity'; 2 | export * from './entities/role.entity'; 3 | 4 | export * from './auth/auth.service'; 5 | export * from './services/user.service'; 6 | export * from './services/role.service'; 7 | export * from './user.module'; -------------------------------------------------------------------------------- /src/user/interfaces/common-result.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CommonResult { 2 | code: number; 3 | message: string; 4 | data?: any; 5 | } -------------------------------------------------------------------------------- /src/user/interfaces/jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayload { 2 | loginName: string; 3 | options?: any; 4 | } 5 | 6 | export interface JwtReply { 7 | accessToken: string; 8 | expiresIn: number; 9 | } -------------------------------------------------------------------------------- /src/user/interfaces/role.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RoleInfoData { 2 | id: number; 3 | name: string; 4 | permissions: { 5 | id: number; 6 | name: string; 7 | action: string; 8 | identify: string; 9 | }[]; 10 | infoItems: { 11 | id: number; 12 | name: string; 13 | description: string; 14 | type: string; 15 | }[]; 16 | } -------------------------------------------------------------------------------- /src/user/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfoData { 2 | id: number; 3 | username: string; 4 | email: string; 5 | mobile: string; 6 | banned: boolean; 7 | recycle: boolean; 8 | createTime: string; 9 | updateTime: string; 10 | userRoles: { 11 | id: number; 12 | name: string 13 | }[]; 14 | } 15 | 16 | export interface CreateUserInput { 17 | username?: string; 18 | email?: string; 19 | mobile?: string; 20 | password: string; 21 | roleIds?: number[]; 22 | nickname?: string; 23 | avater?: string; 24 | } 25 | 26 | export interface UpdateUserInput { 27 | username?: string; 28 | email?: string; 29 | mobile?: string; 30 | password?: string; 31 | roleIds?: { 32 | before: number; 33 | after: number; 34 | }[]; 35 | nickname: string; 36 | avater: string; 37 | } -------------------------------------------------------------------------------- /src/user/resolvers/role.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Mutation, Query, Resolver } from '@nestjs/graphql'; 2 | import { __ as t } from 'i18n'; 3 | 4 | import { CommonResult } from '../interfaces/common-result.interface'; 5 | import { RoleService } from '../services/role.service'; 6 | 7 | @Resolver() 8 | export class RoleResolver { 9 | constructor( 10 | private readonly roleService: RoleService 11 | ) { } 12 | 13 | @Mutation('createRole') 14 | async createRole(req, body: { name: string }): Promise { 15 | await this.roleService.createRole(body.name); 16 | return { code: 200, message: t('Create a role successfully') }; 17 | } 18 | 19 | @Mutation('deleteRole') 20 | async deleteRole(req, body: { id: number }): Promise { 21 | await this.roleService.deleteRole(body.id); 22 | return { code: 200, message: t('Delete role successfully') }; 23 | } 24 | 25 | @Mutation('updateRole') 26 | async updateRole(req, body: { id: number, name: string }): Promise { 27 | await this.roleService.updateRole(body.id, body.name); 28 | return { code: 200, message: t('Update role successfully') }; 29 | } 30 | 31 | @Query('findRoles') 32 | async findRoles(): Promise { 33 | const data = await this.roleService.findRoles(); 34 | return { code: 200, message: t('Query all roles successfully'), data }; 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/user/resolvers/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Inject } from '@nestjs/common'; 2 | import { Mutation, Query, Resolver } from '@nestjs/graphql'; 3 | import { __ as t } from 'i18n'; 4 | 5 | import { CommonResult } from '../interfaces/common-result.interface'; 6 | import { CreateUserInput, UpdateUserInput, UserInfoData } from '../interfaces/user.interface'; 7 | import { UserService } from '../services/user.service'; 8 | import { PagerUtil } from '../utils/pager.util'; 9 | 10 | @Resolver() 11 | export class UserResolver { 12 | constructor( 13 | @Inject(UserService) private readonly userService: UserService, 14 | @Inject(PagerUtil) private readonly pagerUtil: PagerUtil, 15 | ) { } 16 | 17 | @Query('checkPwd') 18 | async checkPwd(req, body: { username: string, password: string }) { 19 | const data = await this.userService.checkPwd(body.username, body.password); 20 | return { code: 200, message: '验证完毕', data }; 21 | } 22 | 23 | @Query('login') 24 | async login(req, body: { username: string, password: string }): Promise { 25 | let data; 26 | if (isNaN(Number(body.password))) { 27 | data = await this.userService.login(body.username, body.password); 28 | } else if (!isNaN(Number(body.password))) { 29 | data = await this.userService.mobileLogin(body.username, parseInt(body.password)); 30 | } 31 | return { code: 200, message: t('Login success'), data: data.tokenInfo }; 32 | } 33 | 34 | @Query('adminLogin') 35 | async adminLogin(req, body: { username: string, password: string }): Promise { 36 | let data; 37 | if (isNaN(Number(body.password))) { 38 | data = await this.userService.login(body.username, body.password); 39 | } else if (!isNaN(Number(body.password))) { 40 | data = await this.userService.mobileLogin(body.username, parseInt(body.password)); 41 | } 42 | const userInfoData = data.userInfoData; 43 | if (userInfoData.username !== 'sadmin' && userInfoData.userRoles.map(v => v.id).includes(1)) { 44 | throw new HttpException(t('You are not authorized to access'), 401); 45 | } 46 | return { code: 200, message: t('Login success'), data: data.tokenInfo }; 47 | } 48 | 49 | @Mutation('register') 50 | async register(req, body: { registerUserInput: CreateUserInput }): Promise { 51 | await this.userService.register(body.registerUserInput); 52 | return { code: 200, message: t('Registration success') }; 53 | } 54 | 55 | @Mutation('createUser') 56 | async createUser(req, body: { createUserInput: CreateUserInput }): Promise { 57 | await this.userService.createUser(body.createUserInput); 58 | return { code: 200, message: t('Create user successfully') }; 59 | } 60 | 61 | @Mutation('addUserRole') 62 | async addUserRole(req, body: { userId: number, roleId: number }): Promise { 63 | await this.userService.addUserRole(body.userId, body.roleId); 64 | return { code: 200, message: t('Add user role successfully') }; 65 | } 66 | 67 | @Mutation('deleteUserRole') 68 | async deleteUserRole(req, body: { userId: number, roleId: number }): Promise { 69 | await this.userService.deleteUserRole(body.userId, body.roleId); 70 | return { code: 200, message: t('Delete user role successfully') }; 71 | } 72 | 73 | @Mutation('banUser') 74 | async banUser(req, body: { userId: number }): Promise { 75 | await this.userService.recycleOrBanUser(body.userId, 'recycle'); 76 | return { code: 200, message: t('Ban user successfully') }; 77 | } 78 | 79 | @Mutation('recycleUser') 80 | async recycleUser(req, body: { userId: number }): Promise { 81 | await this.userService.recycleOrBanUser(body.userId, 'recycle'); 82 | return { code: 200, message: t('Delete user to recycle bin successfully') }; 83 | } 84 | 85 | @Mutation('deleteRecycledUser') 86 | async deleteRecycledUser(req, body: { userId: number[] }): Promise { 87 | await this.userService.deleteUser(body.userId); 88 | return { code: 200, message: t('Delete user in the recycle bin successfully') }; 89 | } 90 | 91 | @Mutation('revertBannedUser') 92 | async revertBannedUser(req, body: { userId: number }): Promise { 93 | await this.userService.revertBannedOrRecycledUser(body.userId, 'banned'); 94 | return { code: 200, message: t('Revert banned user successfully') }; 95 | } 96 | 97 | @Mutation('revertRecycledUser') 98 | async revertRecycledUser(req, body: { userId: number }): Promise { 99 | await this.userService.revertBannedOrRecycledUser(body.userId, 'recycled'); 100 | return { code: 200, message: t('Revert recycled user successfully') }; 101 | } 102 | 103 | @Mutation('updateUserInfoById') 104 | async updateUserInfo(req, body: { userId: number, updateUserInput: UpdateUserInput }): Promise { 105 | await this.userService.updateUserInfo(body.userId, body.updateUserInput); 106 | return { code: 200, message: t('Update user information successfully') }; 107 | } 108 | 109 | @Mutation('updateCurrentUserInfo') 110 | async updateCurrentUserInfo(req, body: { updateCurrentUserInput: UpdateUserInput }, context): Promise { 111 | await this.userService.updateUserInfo(context.user.id, body.updateCurrentUserInput); 112 | return { code: 200, message: t('Update current login user information successfully') }; 113 | } 114 | 115 | @Query('findUserInfoByIds') 116 | async findUserInfoById(req, body: { userIds: number[] }): Promise { 117 | const data = await this.userService.findUserInfoById(body.userIds) as UserInfoData[]; 118 | return { code: 200, message: t('Query the specified users information successfully'), data }; 119 | } 120 | 121 | @Query('findCurrentUserInfo') 122 | async findCurrentUserInfo(req, body, context): Promise { 123 | const data = await this.userService.findUserInfoById(context.user.id) as UserInfoData; 124 | return { code: 200, message: t('Query the current login user information successfully'), data }; 125 | } 126 | 127 | @Query('findUsersInRole') 128 | async findUsersInRole(req, body: { roleId: number }): Promise { 129 | const data = await this.userService.findByRoleId(body.roleId); 130 | return { code: 200, message: t('Query the user under the role successfully'), data }; 131 | } 132 | 133 | @Mutation('findPassword') 134 | async findPassword(req, body: { mobile: string, password: string }): Promise { 135 | await this.userService.findPassword(body.mobile, body.password); 136 | return { code: 200, message: t('Update user information successfully') }; 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /src/user/services/entity-check.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Injectable } from '@nestjs/common'; 2 | import { InjectEntityManager } from '@nestjs/typeorm'; 3 | import { __ as t } from 'i18n'; 4 | import { EntityManager } from 'typeorm'; 5 | 6 | @Injectable() 7 | export class EntityCheckService { 8 | constructor( 9 | @InjectEntityManager() private entityManager: EntityManager 10 | ) { } 11 | 12 | async checkNameExist(entityClass: any, name: string) { 13 | const exist = await this.entityManager.findOne(entityClass, { name }); 14 | if (exist) { 15 | throw new HttpException(t('Name already exists'), 409); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/user/services/role.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, Inject, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { __ as t } from 'i18n'; 4 | import { Repository } from 'typeorm'; 5 | 6 | import { Role } from '../entities/role.entity'; 7 | import { EntityCheckService } from './entity-check.service'; 8 | 9 | @Injectable() 10 | export class RoleService { 11 | constructor( 12 | @Inject(EntityCheckService) private readonly entityCheckService: EntityCheckService, 13 | @InjectRepository(Role) private readonly roleRepo: Repository, 14 | ) { } 15 | 16 | /** 17 | * Create a role 18 | * 19 | * @param name The role's name 20 | */ 21 | async createRole(name: string) { 22 | await this.entityCheckService.checkNameExist(Role, name); 23 | await this.roleRepo.save(this.roleRepo.create({ name })); 24 | } 25 | 26 | /** 27 | * Update the specified role's name 28 | * 29 | * @param id The specified role's id 30 | * @param name The name to be update 31 | */ 32 | async updateRole(id: number, name: string) { 33 | const role = await this.roleRepo.findOne(id); 34 | if (!role) { 35 | throw new HttpException(t('The role id of %s does not exist', id.toString()), 404); 36 | } 37 | 38 | if (name !== role.name) { 39 | await this.entityCheckService.checkNameExist(Role, name); 40 | } 41 | 42 | role.name = name; 43 | await this.roleRepo.save(role); 44 | } 45 | 46 | /** 47 | * Delete role 48 | * 49 | * @param id The specified role's id 50 | */ 51 | async deleteRole(id: number) { 52 | const role = await this.roleRepo.findOne(id, { relations: ['permissions'] }); 53 | if (!role) { 54 | throw new HttpException(t('The role id of %s does not exist', id.toString()), 404); 55 | } 56 | 57 | try { 58 | await this.roleRepo.remove(role); 59 | } catch (err) { 60 | throw new HttpException(t('Database error %s', err.toString()), 500); 61 | } 62 | } 63 | 64 | /** 65 | * Query all roles 66 | */ 67 | async findRoles() { 68 | return this.roleRepo.find(); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /src/user/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, HttpException, Inject, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { SmsComponent } from '@notadd/addon-sms'; 4 | import { __ as t } from 'i18n'; 5 | import { Repository } from 'typeorm'; 6 | import * as _ from 'underscore'; 7 | 8 | import { AuthService } from '../auth/auth.service'; 9 | import { User } from '../entities/user.entity'; 10 | import { CreateUserInput, UpdateUserInput, UserInfoData } from '../interfaces/user.interface'; 11 | import { CryptoUtil } from '../utils/crypto.util'; 12 | import { RoleService } from './role.service'; 13 | 14 | @Injectable() 15 | export class UserService { 16 | constructor( 17 | @InjectRepository(User) private readonly userRepo: Repository, 18 | @Inject(CryptoUtil) private readonly cryptoUtil: CryptoUtil, 19 | @Inject(forwardRef(() => AuthService)) private readonly authService: AuthService, 20 | @Inject(RoleService) private readonly roleService: RoleService, 21 | @Inject('SmsComponentToken') private readonly smsComponentProvider: SmsComponent, 22 | ) { } 23 | 24 | /** 25 | * Cteate a user 26 | * 27 | * @param user The user object 28 | */ 29 | async createUser(createUserInput: CreateUserInput): Promise { 30 | if (!(createUserInput.username || createUserInput.mobile || createUserInput.email)) { 31 | throw new HttpException(t('Please make sure the username, mobile phone number, and email exist at least one'), 406); 32 | } 33 | if (createUserInput.username && await this.userRepo.findOne({ where: { username: createUserInput.username } })) { 34 | throw new HttpException(t('Username already exists'), 409); 35 | } 36 | if (createUserInput.mobile && await this.userRepo.findOne({ where: { mobile: createUserInput.mobile } })) { 37 | throw new HttpException(t('Mobile already exists'), 409); 38 | } 39 | if (createUserInput.email && await this.userRepo.findOne({ where: { email: createUserInput.email } })) { 40 | throw new HttpException(t('Email already exists'), 409); 41 | } 42 | 43 | if (!createUserInput.mobile && createUserInput.username) { 44 | createUserInput.mobile = createUserInput.username; 45 | } 46 | 47 | createUserInput.password = await this.cryptoUtil.encryptPassword(createUserInput.password); 48 | if (createUserInput.email) createUserInput.email = createUserInput.email.toLocaleLowerCase(); 49 | const user = await this.userRepo.save(this.userRepo.create(createUserInput)); 50 | 51 | if (createUserInput.roleIds && createUserInput.roleIds.length) { 52 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(user).add(createUserInput.roleIds); 53 | } 54 | } 55 | 56 | /** 57 | * Add a role to the user 58 | * 59 | * @param userId The specified user id 60 | * @param roleId The specified role id 61 | */ 62 | async addUserRole(userId: number, roleId: number) { 63 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(userId).add(roleId); 64 | } 65 | 66 | /** 67 | * Delete a role from the user 68 | * 69 | * @param userId The specified user id 70 | * @param roleId The specified role id 71 | */ 72 | async deleteUserRole(userId: number, roleId: number) { 73 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(userId).remove(roleId); 74 | } 75 | 76 | /** 77 | * Delete user to recycle bin or ban user 78 | * 79 | * @param id The specified user id 80 | */ 81 | async recycleOrBanUser(id: number, action: 'recycle' | 'ban'): Promise { 82 | const user = await this.findOneById(id); 83 | if (action === 'recycle') { 84 | user.recycle = true; 85 | } 86 | if (action === 'ban') { 87 | user.banned = true; 88 | } 89 | await this.userRepo.save(user); 90 | } 91 | 92 | /** 93 | * Delete users in the recycle bin 94 | * 95 | * @param id The specified user id 96 | */ 97 | async deleteUser(id: number | number[]): Promise { 98 | const exist = await this.findUserInfoById(id); 99 | const a = exist.map(item => item.id); 100 | // tslint:disable-next-line:prefer-for-of 101 | for (let i = 0; i < a.length; i++) { 102 | const user = await this.userRepo.findOne(a[i], { relations: ['roles'] }); 103 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(user).remove(user.roles); 104 | await this.userRepo.remove(user); 105 | } 106 | } 107 | 108 | /** 109 | * Revert user from which was banned or recycled 110 | * 111 | * @param id The specified user id 112 | */ 113 | async revertBannedOrRecycledUser(id: number, status: 'recycled' | 'banned') { 114 | const user = await this.findOneById(id); 115 | if (status === 'recycled') { 116 | user.recycle = false; 117 | } 118 | if (status === 'banned') { 119 | user.banned = false; 120 | } 121 | await this.userRepo.save(user); 122 | } 123 | 124 | /** 125 | * Update user's information 126 | * 127 | * @param id The specified user id 128 | * @param updateUserInput The information to be update 129 | */ 130 | async updateUserInfo(id: number, updateUserInput: UpdateUserInput): Promise { 131 | const user = await this.userRepo.findOne(id); 132 | 133 | if (updateUserInput.username && updateUserInput.username !== user.username) { 134 | if (await this.userRepo.findOne({ where: { username: updateUserInput.username } })) { 135 | throw new HttpException(t('Username already exists'), 409); 136 | } 137 | await this.userRepo.update(user.id, { username: updateUserInput.username }); 138 | } 139 | if (updateUserInput.mobile && updateUserInput.mobile !== user.mobile) { 140 | if (await this.userRepo.findOne({ where: { mobile: updateUserInput.mobile } })) { 141 | throw new HttpException(t('Mobile already exists'), 409); 142 | } 143 | await this.userRepo.update(user.id, { mobile: updateUserInput.mobile }); 144 | } 145 | if (updateUserInput.email && updateUserInput.email !== user.email) { 146 | if (await this.userRepo.findOne({ where: { email: updateUserInput.email } })) { 147 | throw new HttpException(t('Email already exists'), 409); 148 | } 149 | await this.userRepo.update(user.id, { email: updateUserInput.email.toLocaleLowerCase() }); 150 | } 151 | 152 | if (updateUserInput.password) { 153 | const newPassword = await this.cryptoUtil.encryptPassword(updateUserInput.password); 154 | await this.userRepo.update(user.id, { password: newPassword }); 155 | } 156 | if (updateUserInput.roleIds && updateUserInput.roleIds.length) { 157 | updateUserInput.roleIds.forEach(async roleId => { 158 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(user).remove(roleId.before); 159 | await this.userRepo.createQueryBuilder('user').relation(User, 'roles').of(user).add(roleId.after); 160 | }); 161 | } 162 | } 163 | 164 | /** 165 | * Query the user by role ID 166 | * 167 | * @param roleId The specified role id 168 | */ 169 | async findByRoleId(roleId: number) { 170 | const users = await this.userRepo.createQueryBuilder('user') 171 | .leftJoinAndSelect('user.roles', 'roles') 172 | .where('roles.id = :roleId', { roleId }) 173 | .andWhere('user.recycle = false') 174 | .getMany(); 175 | if (!users.length) { 176 | throw new HttpException(t('No users belong to this role'), 404); 177 | } 178 | return this.findUserInfoById(users.map(user => user.id)) as Promise; 179 | } 180 | 181 | /** 182 | * Querying users and their associated information by username 183 | * 184 | * @param username username 185 | */ 186 | async findOneWithRoles(loginName: string): Promise { 187 | const user = await this.userRepo.createQueryBuilder('user') 188 | .leftJoinAndSelect('user.roles', 'roles') 189 | .where('user.username = :loginName', { loginName }) 190 | .orWhere('user.mobile = :loginName', { loginName }) 191 | .orWhere('user.email = :loginName', { loginName: loginName.toLocaleLowerCase() }) 192 | .getOne(); 193 | 194 | if (!user) { 195 | throw new HttpException(t('User does not exist'), 404); 196 | } 197 | return user; 198 | } 199 | 200 | /** 201 | * Querying user information by user ID 202 | * 203 | * @param id The specified user id 204 | */ 205 | async findUserInfoById(id: number | number[]): Promise { 206 | const userQb = this.userRepo.createQueryBuilder('user') 207 | .leftJoinAndSelect('user.roles', 'roles'); 208 | 209 | if (id instanceof Array) { 210 | const userInfoData: UserInfoData[] = []; 211 | const users = await userQb.whereInIds(id).getMany(); 212 | for (const user of users) { 213 | (userInfoData as UserInfoData[]).push(this.refactorUserData(user)); 214 | } 215 | return userInfoData; 216 | } else { 217 | const user = await userQb.where('user.id = :id', { id }).getOne(); 218 | return this.refactorUserData(user); 219 | } 220 | } 221 | 222 | /** 223 | * user login by username or email 224 | * 225 | * @param loginName loginName: username or email 226 | * @param password password 227 | */ 228 | async login(loginName: string, password: string) { 229 | const user = await this.userRepo.createQueryBuilder('user') 230 | .leftJoinAndSelect('user.roles', 'roles') 231 | .where('user.username = :loginName', { loginName }) 232 | .orWhere('user.mobile = :loginName', { loginName }) 233 | .orWhere('user.email = :loginName', { loginName: loginName.toLocaleLowerCase() }) 234 | .getOne(); 235 | 236 | await this.checkUserStatus(user); 237 | if (!await this.cryptoUtil.checkPassword(password, user.password)) { 238 | throw new HttpException(t('invalid password'), 406); 239 | } 240 | 241 | const userInfoData = this.refactorUserData(user); 242 | 243 | const tokenInfo = await this.authService.createToken({ loginName }); 244 | return { tokenInfo, userInfoData }; 245 | } 246 | 247 | async checkPwd(loginName: string, password: string) { 248 | const user = await this.userRepo.createQueryBuilder('user') 249 | .where('user.username = :loginName', { loginName }) 250 | .orWhere('user.mobile = :loginName', { loginName }) 251 | .orWhere('user.email = :loginName', { loginName: loginName.toLocaleLowerCase() }) 252 | .getOne(); 253 | if (!await this.cryptoUtil.checkPassword(password, user.password)) { 254 | return false; 255 | } 256 | return true; 257 | } 258 | 259 | /** 260 | * user login by mobile (use tencent cloud sms service) 261 | * 262 | * @param mobile mobile 263 | * @param validationCode validationCode 264 | */ 265 | async mobileLogin(mobile: string, validationCode: number) { 266 | await this.smsComponentProvider.smsValidator(mobile, validationCode); 267 | 268 | const user = await this.userRepo.findOne({ mobile }, { relations: ['roles'] }); 269 | await this.checkUserStatus(user); 270 | 271 | const userInfoData = this.refactorUserData(user); 272 | 273 | const tokenInfo = await this.authService.createToken({ loginName: mobile }); 274 | return { tokenInfo, userInfoData }; 275 | } 276 | 277 | /** 278 | * Ordinary user registration 279 | * 280 | * @param username username 281 | * @param password password 282 | */ 283 | async register(createUserInput: CreateUserInput): Promise { 284 | createUserInput.roleIds = [1]; 285 | await this.createUser(createUserInput); 286 | } 287 | 288 | private checkUserStatus(user: User) { 289 | if (!user) throw new HttpException(t('User does not exist'), 404); 290 | if (user.banned || user.recycle) throw new HttpException(t('User is banned'), 400); 291 | } 292 | 293 | /** 294 | * Query users by ID 295 | * 296 | * @param id The specified user id 297 | */ 298 | private async findOneById(id: number): Promise { 299 | const exist = this.userRepo.findOne(id); 300 | if (!exist) { 301 | throw new HttpException(t('User does not exist'), 404); 302 | } 303 | return exist; 304 | } 305 | 306 | /** 307 | * Refactor the user information data 308 | * 309 | * @param user The user object 310 | */ 311 | private refactorUserData(user: User) { 312 | const userInfoData: UserInfoData = { 313 | id: user.id, 314 | username: user.username, 315 | email: user.email, 316 | mobile: user.mobile, 317 | banned: user.banned, 318 | recycle: user.recycle, 319 | createTime: user.createTime, 320 | updateTime: user.updateTime, 321 | userRoles: user.roles, 322 | }; 323 | return userInfoData; 324 | } 325 | 326 | async findPassword(mobile: string, password: string): Promise { 327 | const user = await this.userRepo.findOne({ where: { mobile } }); 328 | if (!user) { 329 | throw new HttpException(t('User does not exist'), 404); 330 | } 331 | const newPassword = await this.cryptoUtil.encryptPassword(password); 332 | user.password = newPassword; 333 | await this.userRepo.save(await this.userRepo.create(user)); 334 | } 335 | 336 | } -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Inject, Module, OnModuleInit } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { ModulesContainer } from '@nestjs/core/injector/modules-container'; 4 | import { MetadataScanner } from '@nestjs/core/metadata-scanner'; 5 | import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; 6 | import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; 7 | import { __ as t, configure as i18nConfigure } from 'i18n'; 8 | import { join } from 'path'; 9 | import { Repository } from 'typeorm'; 10 | import { AuthService } from './auth/auth.service'; 11 | import { AUTH_TOKEN_WHITE_LIST } from './constants/auth.constant'; 12 | import { Role } from './entities/role.entity'; 13 | import { User } from './entities/user.entity'; 14 | import { RoleResolver } from './resolvers/role.resolver'; 15 | import { UserResolver } from './resolvers/user.resolver'; 16 | import { EntityCheckService } from './services/entity-check.service'; 17 | import { RoleService } from './services/role.service'; 18 | import { UserService } from './services/user.service'; 19 | import { CryptoUtil } from './utils/crypto.util'; 20 | import { PagerUtil } from './utils/pager.util'; 21 | import { SmsModule } from '@notadd/addon-sms'; 22 | 23 | @Global() 24 | @Module({ 25 | imports: [ 26 | TypeOrmModule.forFeature([ User, Role]), 27 | SmsModule 28 | ], 29 | controllers: [], 30 | providers: [ 31 | AuthService, 32 | EntityCheckService, 33 | UserResolver, UserService, 34 | RoleResolver, RoleService, 35 | CryptoUtil, PagerUtil 36 | ], 37 | exports: [AuthService, UserService, RoleService] 38 | }) 39 | export class UserModule implements OnModuleInit { 40 | private readonly metadataScanner: MetadataScanner; 41 | 42 | constructor( 43 | @Inject(UserService) private readonly userService: UserService, 44 | @Inject(ModulesContainer) private readonly modulesContainer: ModulesContainer, 45 | @InjectRepository(Role) private readonly roleRepo: Repository, 46 | @InjectRepository(User) private readonly userRepo: Repository, 47 | ) { 48 | this.metadataScanner = new MetadataScanner(); 49 | } 50 | 51 | static forRoot(options: { i18n: 'en-US' | 'zh-CN', authTokenWhiteList?: string[] }): DynamicModule { 52 | if (!existsSync('src/i18n')) { 53 | mkdirSync(join('src/i18n')); 54 | writeFileSync(join('src/i18n', 'zh-CN.json'), readFileSync(__dirname + '/i18n/zh-CN.json')); 55 | writeFileSync(join('src/i18n', 'en-US.json'), readFileSync(__dirname + '/i18n/en-US.json')); 56 | } 57 | i18nConfigure({ 58 | locales: ['en-US', 'zh-CN'], 59 | defaultLocale: options.i18n, 60 | directory: 'src/i18n' 61 | }); 62 | if (options.authTokenWhiteList) { 63 | options.authTokenWhiteList.push(...['IntrospectionQuery', 'login', 'adminLogin', 'register']); 64 | } else { 65 | options.authTokenWhiteList = ['IntrospectionQuery', 'login', 'adminLogin', 'register']; 66 | } 67 | return { 68 | providers: [{ provide: AUTH_TOKEN_WHITE_LIST, useValue: options.authTokenWhiteList }], 69 | module: UserModule 70 | }; 71 | } 72 | 73 | async onModuleInit() { 74 | await this.createDefaultRole(); 75 | await this.createSuperAdmin(); 76 | } 77 | 78 | /** 79 | * Create a default ordinary user role 80 | */ 81 | private async createDefaultRole() { 82 | const ordinary = await this.roleRepo.findOne({ where: { name: '普通用户' } }); 83 | if (!ordinary) { 84 | await this.roleRepo.save(this.roleRepo.create({ 85 | id: 1, 86 | name: t('ordinary user') 87 | })); 88 | } 89 | 90 | const admin = await this.roleRepo.findOne({ where: { name: '管理员' } }); 91 | if (!admin) { 92 | await this.roleRepo.save(this.roleRepo.create({ 93 | id: 2, 94 | name: '管理员' 95 | })); 96 | } 97 | } 98 | 99 | /** 100 | * Create a system super administrator 101 | */ 102 | private async createSuperAdmin() { 103 | const sadmin = await this.userRepo.findOne({ where: { username: 'sadmin' } }); 104 | if (sadmin) return; 105 | await this.userService.createUser({ username: 'sadmin', password: 'sadmin' }); 106 | } 107 | 108 | } -------------------------------------------------------------------------------- /src/user/utils/crypto.util.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as bcrypt from 'bcryptjs'; 3 | 4 | /** 5 | * Crypto util 6 | */ 7 | @Injectable() 8 | export class CryptoUtil { 9 | 10 | /** 11 | * Encrypt the password 12 | * 13 | * @param password password 14 | */ 15 | async encryptPassword(password: string): Promise { 16 | return bcrypt.hash(password, bcrypt.genSaltSync()); 17 | } 18 | 19 | /** 20 | * Check if the password is correct 21 | * 22 | * @param password password 23 | * @param passwordHash password of hash 24 | */ 25 | async checkPassword(password: string, passwordHash: string): Promise { 26 | return bcrypt.compare(password, passwordHash); 27 | } 28 | } -------------------------------------------------------------------------------- /src/user/utils/pager.util.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'underscore'; 2 | import { Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class PagerUtil { 6 | getPager(totalItems, currentPage, pageSize) { 7 | if (pageSize === undefined || pageSize === 0) { 8 | pageSize = 1; 9 | } 10 | if (currentPage === undefined || currentPage === 0) { 11 | currentPage = 1; 12 | } 13 | const totalPages = Math.ceil(totalItems / pageSize); 14 | let startPage, endPage; 15 | if (totalPages <= 10) { 16 | startPage = 1; 17 | endPage = totalPages; 18 | } 19 | else { 20 | if (currentPage <= 6) { 21 | startPage = 1; 22 | endPage = 10; 23 | } 24 | else if (currentPage + 4 >= totalPages) { 25 | startPage = totalPages - 9; 26 | endPage = totalPages; 27 | } 28 | else { 29 | startPage = currentPage - 5; 30 | endPage = currentPage + 4; 31 | } 32 | } 33 | const startIndex = (currentPage - 1) * pageSize; 34 | const endIndex = Math.min(startIndex + pageSize - 1, totalItems - 1); 35 | const pages = _.range(startPage, endPage + 1); 36 | return { 37 | totalItems, 38 | currentPage, 39 | pageSize, 40 | totalPages, 41 | startPage, 42 | endPage, 43 | startIndex, 44 | endIndex, 45 | pages 46 | }; 47 | } 48 | } -------------------------------------------------------------------------------- /starter/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { GraphqlConfig } from './graphql.config'; 5 | import { CmsModule } from '../src/cms/cms.module'; 6 | import { UserModule } from '../src/user/user.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | GraphQLModule.forRootAsync({ 11 | useClass: GraphqlConfig 12 | }), 13 | TypeOrmModule.forRoot(), 14 | CmsModule, 15 | UserModule 16 | ], 17 | controllers: [], 18 | providers: [], 19 | exports: [] 20 | }) 21 | export class AppModule { } -------------------------------------------------------------------------------- /starter/graphql.config.ts: -------------------------------------------------------------------------------- 1 | import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql'; 2 | import { Injectable, Inject } from '@nestjs/common'; 3 | import * as GraphQLJSON from 'graphql-type-json'; 4 | 5 | @Injectable() 6 | export class GraphqlConfig implements GqlOptionsFactory { 7 | constructor( 8 | ) {} 9 | createGqlOptions(): GqlModuleOptions { 10 | return { 11 | typePaths: ['./**/*.types.graphql'], 12 | resolvers: { JSON: GraphQLJSON }, 13 | context: ({ req }) => ({ req }) 14 | }; 15 | } 16 | } -------------------------------------------------------------------------------- /starter/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import * as fs from 'fs'; 3 | import { AppModule } from './app.module'; 4 | 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | } 10 | 11 | bootstrap(); -------------------------------------------------------------------------------- /starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "./*.ts" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "baseUrl": "./", 5 | "declaration": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "noImplicitAny": false, 10 | "noLib": false, 11 | "lib": [ 12 | "es6" 13 | ], 14 | "noUnusedLocals": false, 15 | "removeComments": true, 16 | "strict": false, 17 | "strictPropertyInitialization": false, 18 | "target": "es6", 19 | "outDir": "package" 20 | }, 21 | "include": [ 22 | "./src/*.ts", 23 | "./src/**/*.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "**/*.spec.ts" 28 | ] 29 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "array-type": [ 11 | true, 12 | "array" 13 | ], 14 | "ban-types": { 15 | "options": [ 16 | [ 17 | "Object", 18 | "Avoid using the `Object` type. Did you mean `object`?" 19 | ], 20 | [ 21 | "Function", 22 | "Avoid using the `Function` type. Prefer a specific function type, like `() => void`, or use `ts.AnyFunction`." 23 | ], 24 | [ 25 | "Boolean", 26 | "Avoid using the `Boolean` type. Did you mean `boolean`?" 27 | ], 28 | [ 29 | "Number", 30 | "Avoid using the `Number` type. Did you mean `number`?" 31 | ], 32 | [ 33 | "String", 34 | "Avoid using the `String` type. Did you mean `string`?" 35 | ] 36 | ] 37 | }, 38 | "class-name": true, 39 | "comment-format": [ 40 | true, 41 | "check-space" 42 | ], 43 | "curly": [ 44 | true, 45 | "ignore-same-line" 46 | ], 47 | "indent": [ 48 | true, 49 | "spaces", 50 | 4 51 | ], 52 | "max-line-length": [ 53 | true, 54 | 200 55 | ], 56 | "quotemark": [ 57 | true, 58 | "single" 59 | ], 60 | "semicolon": [ 61 | true, 62 | "always" 63 | ], 64 | "interface-name": [ 65 | false 66 | ], 67 | "interface-over-type-literal": true, 68 | "jsdoc-format": true, 69 | "linebreak-style": false, 70 | "next-line": false, 71 | "no-inferrable-types": true, 72 | "no-internal-module": true, 73 | "no-null-keyword": true, 74 | "no-switch-case-fall-through": true, 75 | "no-trailing-whitespace": [ 76 | true, 77 | "ignore-template-strings" 78 | ], 79 | "no-var-keyword": true, 80 | "object-literal-shorthand": true, 81 | "one-line": [ 82 | true, 83 | "check-open-brace", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "prefer-conditional-expression": [ 88 | true, 89 | "check-else-if" 90 | ], 91 | "prefer-for-of": true, 92 | "space-within-parens": true, 93 | "triple-equals": true, 94 | "typedef-whitespace": [ 95 | true, 96 | { 97 | "call-signature": "nospace", 98 | "index-signature": "nospace", 99 | "parameter": "nospace", 100 | "property-declaration": "nospace", 101 | "variable-declaration": "nospace" 102 | } 103 | ], 104 | "whitespace": [ 105 | true, 106 | "check-branch", 107 | "check-decl", 108 | "check-operator", 109 | "check-separator", 110 | "check-type" 111 | ], 112 | "no-implicit-dependencies": false, 113 | "object-literal-key-quotes": [ 114 | true, 115 | "consistent-as-needed" 116 | ], 117 | "variable-name": [ 118 | true, 119 | "ban-keywords", 120 | "check-format", 121 | "allow-leading-underscore", 122 | "allow-pascal-case" 123 | ], 124 | "arrow-parens": false, 125 | "arrow-return-shorthand": true, 126 | "forin": false, 127 | "member-access": false, 128 | "no-conditional-assignment": false, 129 | "no-console": false, 130 | "no-debugger": false, 131 | "no-empty": false, 132 | "no-empty-interface": false, 133 | "no-eval": false, 134 | "no-object-literal-type-assertion": false, 135 | "no-shadowed-variable": false, 136 | "no-submodule-imports": false, 137 | "no-var-requires": false, 138 | "ordered-imports": false, 139 | "radix": false, 140 | "trailing-comma": false, 141 | "align": false, 142 | "eofline": false, 143 | "no-consecutive-blank-lines": false, 144 | "space-before-function-paren": false, 145 | "ban-comma-operator": false, 146 | "max-classes-per-file": false, 147 | "member-ordering": false, 148 | "no-angle-bracket-type-assertion": false, 149 | "no-bitwise": false, 150 | "no-namespace": false, 151 | "no-reference": false, 152 | "object-literal-sort-keys": false, 153 | "one-variable-per-declaration": false, 154 | "type-operator-spacing": false, 155 | "no-type-assertion-whitespace": false, 156 | "object-literal-surrounding-space": false, 157 | "no-increment-decrement": false, 158 | "no-in-operator": false, 159 | "no-double-space": false, 160 | "no-unnecessary-type-assertion-2": false, 161 | "no-bom": false, 162 | "boolean-trivia": false, 163 | "debug-assert": false, 164 | "no-unused-expression": false 165 | } 166 | } --------------------------------------------------------------------------------