├── .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 | }
--------------------------------------------------------------------------------