├── .autod.conf.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── app.ts ├── app ├── controller │ ├── BaseController.ts │ ├── captcha.ts │ ├── login.ts │ ├── login_out.ts │ ├── system │ │ ├── dictionary.ts │ │ ├── resource.ts │ │ ├── role.ts │ │ └── user.ts │ └── upload.ts ├── extend │ ├── context.ts │ ├── error │ │ └── index.ts │ └── helper.ts ├── middleware │ ├── error_handler.ts │ └── validate.ts ├── model │ ├── dictionary.ts │ ├── resource.ts │ ├── role.ts │ └── user.ts ├── router.ts ├── schedule │ └── clear_file.ts ├── service │ ├── captcha.ts │ ├── login.ts │ ├── login_out.ts │ ├── system │ │ ├── dictionary.ts │ │ ├── resource.ts │ │ ├── role.ts │ │ └── user.ts │ └── upload.ts ├── settings.ts ├── utils │ ├── crypto.ts │ └── date.ts └── validator │ └── index.ts ├── appveyor.yml ├── config ├── config.default.ts ├── config.local.ts ├── config.prod.ts └── plugin.ts ├── nkm-admin.postman_collection.json ├── nkm_admin.sql ├── package.json ├── test └── app │ ├── controller │ └── home.test.ts │ └── service │ └── Test.test.ts ├── tsconfig.json ├── typings └── index.d.ts └── yarn.lock /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | plugin: 'autod-egg', 6 | prefix: '^', 7 | devprefix: '^', 8 | exclude: [ 9 | 'test/fixtures', 10 | 'coverage', 11 | ], 12 | dep: [ 13 | 'egg', 14 | 'egg-scripts', 15 | ], 16 | devdep: [ 17 | 'autod', 18 | 'autod-egg', 19 | 'egg-bin', 20 | 'tslib', 21 | 'typescript', 22 | ], 23 | keep: [ 24 | ], 25 | semver: [ 26 | ], 27 | test: 'scripts', 28 | }; 29 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-egg/typescript', 'eslint:recommended'], 3 | parserOptions: { 4 | project: './tsconfig.json' 5 | }, 6 | rules: { 7 | semi: ['error', 'never'], 8 | '@typescript-eslint/semi': 0, 9 | 'comma-dangle': ['error', 'never'], 10 | 'array-bracket-spacing': 0, 11 | 'default-case': 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | .vscode 9 | *.swp 10 | *.js 11 | !.autod.conf.js 12 | !.eslintrc.js 13 | 14 | .github/ 15 | 16 | app/**/*.js 17 | test/**/*.js 18 | config/**/*.js 19 | app/**/*.map 20 | test/**/*.map 21 | config/**/*.map 22 | 23 | app/public/upload 24 | typings/app 25 | typings/config 26 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | build: 2 | script: 3 | - echo '任务开始' 4 | - cd /htdocs/admin-test/nkm-server-ts 5 | - yarn stop 6 | - git pull 7 | - yarn bootstrap:test 8 | - echo '任务结束' 9 | tags: 10 | - my-runner 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - '8' 5 | before_install: 6 | - npm i npminstall -g 7 | install: 8 | - npminstall 9 | script: 10 | - npm run ci 11 | after_script: 12 | - npminstall codecov && codecov 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nkm-admin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 后台服务端 2 | 3 | - 技术栈:Nodejs+Eggjs+Typescript+Mysql+Redis 4 | - 接口文档都在[nkm-admin.postman_collection.json](./nkm-admin.postman_collection.json)文件,导入到postman即可 5 | - 数据库导入[nkm_admin.sql](./nkm_admin.sql)文件 6 | - 管理员用户账号密码:admin/123456 7 | - 其他配置参考eggjs文档[https://eggjs.org/zh-cn/basics/structure.html](https://eggjs.org/zh-cn/basics/structure.html) 8 | 9 | ## 用户角色权限说明 10 | 1. 新增资源 11 | 1. 新增角色然后角色关联资源 12 | 1. 新增用户然后用户关联角色(目前已实现单用户多角色功能) 13 | 14 | ## QuickStart 15 | 16 | ### Development 17 | 18 | ```bash 19 | $ npm i 20 | $ npm run dev 21 | $ open http://localhost:7001/ 22 | ``` 23 | 24 | Don't tsc compile at development mode, if you had run `tsc` then you need to `npm run clean` before `npm run dev`. 25 | 26 | ### Deploy 27 | 28 | ```bash 29 | $ npm run tsc 30 | $ npm start 31 | ``` 32 | 33 | ### Npm Scripts 34 | 35 | - Use `npm run lint` to check code style 36 | - Use `npm test` to run unit test 37 | - se `npm run clean` to clean compiled js at development mode once 38 | 39 | ### Requirement 40 | 41 | - Node.js 8.x 42 | - Typescript 2.8+ 43 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | import validator from './app/validator' 3 | 4 | class AppBootHook { 5 | private app: Application 6 | constructor(app: Application) { 7 | this.app = app 8 | } 9 | 10 | // 配置文件即将加载 11 | configWillLoad() { 12 | console.log('配置文件即将加载') 13 | } 14 | 15 | configDidLoad() { 16 | console.log('配置文件加载完成') 17 | } 18 | 19 | didLoad() { 20 | console.log('文件加载完成') 21 | } 22 | 23 | // 插件启动完毕 24 | async willReady() { 25 | for (const [_k, _v] of Object.entries(validator)) { 26 | this.app.validator.addRule(_k, (...[, value]) => _v(value)) 27 | } 28 | console.log('插件启动完毕') 29 | } 30 | 31 | serverDidReady() { 32 | console.log('应用启动完成') 33 | } 34 | } 35 | 36 | module.exports = AppBootHook 37 | -------------------------------------------------------------------------------- /app/controller/BaseController.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from 'egg' 2 | import { objectKeyToCamelCase } from '@xuanmo/javascript-utils' 3 | 4 | export default class BaseController extends Controller { 5 | public success(response: { 6 | data?: any; 7 | message?: string; 8 | success?: boolean; 9 | code?: string; 10 | count?: number; 11 | } = {}) { 12 | return { 13 | ...this.ctx.responseStruc(), 14 | ...response, 15 | data: objectKeyToCamelCase(response.data, 'dataValues') 16 | } 17 | } 18 | 19 | public fail(response: Response) { 20 | return { 21 | ...this.ctx.responseStruc(this.ctx.errorMsg.common.failed), 22 | ...response, 23 | success: false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/controller/captcha.ts: -------------------------------------------------------------------------------- 1 | import BaseController from './BaseController' 2 | 3 | export default class Captcha extends BaseController { 4 | public async init() { 5 | this.ctx.body = this.success({ 6 | data: await this.ctx.service.captcha.init() 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/controller/login.ts: -------------------------------------------------------------------------------- 1 | import BaseController from './BaseController' 2 | 3 | export default class Login extends BaseController { 4 | public async login() { 5 | const { ctx } = this 6 | const data = await ctx.service.login.login(ctx.request.body) 7 | ctx.body = this.success({ 8 | data 9 | }) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/controller/login_out.ts: -------------------------------------------------------------------------------- 1 | import BaseController from './BaseController' 2 | 3 | export default class LoginOut extends BaseController { 4 | public async loginOut() { 5 | const { ctx } = this 6 | await ctx.service.loginOut.loginOut(ctx.headers.token) 7 | ctx.body = this.success() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/controller/system/dictionary.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../BaseController' 2 | 3 | export default class Dictionary extends BaseController { 4 | public async getTree() { 5 | const data = await this.service.system.dictionary.getTree() 6 | this.ctx.body = this.success({ 7 | data 8 | }) 9 | } 10 | 11 | public async save() { 12 | await this.service.system.dictionary.save(this.ctx.request.body) 13 | this.ctx.body = this.success() 14 | } 15 | 16 | public async del() { 17 | await this.service.system.dictionary.del(this.ctx.request.body.id) 18 | this.ctx.body = this.success() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/controller/system/resource.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../BaseController' 2 | 3 | export default class Resource extends BaseController { 4 | public async save() { 5 | const { ctx } = this 6 | await ctx.service.system.resource.save(ctx.request.body) 7 | ctx.body = this.success() 8 | } 9 | 10 | public async getTree() { 11 | const { ctx } = this 12 | const data = await ctx.service.system.resource.getTree() 13 | ctx.body = this.success({ 14 | data 15 | }) 16 | } 17 | 18 | public async getList() { 19 | const { ctx } = this 20 | const data = await ctx.service.system.resource.getList() 21 | ctx.body = this.success({ 22 | data 23 | }) 24 | } 25 | 26 | public async del() { 27 | const { ctx } = this 28 | await ctx.service.system.resource.del(ctx.request.body.id) 29 | ctx.body = this.success() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/controller/system/role.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../BaseController' 2 | 3 | export default class Role extends BaseController { 4 | public async getList() { 5 | const data = await this.ctx.service.system.role.getList() 6 | this.ctx.body = this.success({ 7 | data 8 | }) 9 | } 10 | 11 | public async save() { 12 | await this.ctx.service.system.role.save(this.ctx.request.body) 13 | this.ctx.body = this.success() 14 | } 15 | 16 | public async del() { 17 | await this.ctx.service.system.role.del(this.ctx.request.body.id) 18 | this.ctx.body = this.success() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/controller/system/user.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../BaseController' 2 | 3 | export default class User extends BaseController { 4 | public async getUserList() { 5 | const { ctx } = this 6 | const { rows: data, count } = await ctx.service.system.user.getUserList({ 7 | ...ctx.conversionPagination({ 8 | page: +ctx.query.page, 9 | limit: +ctx.query.limit 10 | }) 11 | }) 12 | ctx.body = this.success({ 13 | data, 14 | count 15 | }) 16 | } 17 | 18 | public async registered() { 19 | const { ctx } = this 20 | await ctx.service.system.user.registered(ctx.request.body) 21 | ctx.body = this.success() 22 | } 23 | 24 | public async del() { 25 | const { ctx } = this 26 | await ctx.service.system.user.del(ctx.request.body.id) 27 | ctx.body = this.success() 28 | } 29 | 30 | public async modifyStatus() { 31 | const { ctx } = this 32 | await ctx.service.system.user.modifyStatus(ctx.request.body.id, ctx.request.body.status) 33 | ctx.body = this.success() 34 | } 35 | 36 | public async modifyRole() { 37 | const { ctx } = this 38 | await ctx.service.system.user.modifyRole(ctx.request.body.id, ctx.request.body.role) 39 | ctx.body = this.success() 40 | } 41 | 42 | public async resetPassword() { 43 | const { ctx } = this 44 | await ctx.service.system.user.resetPassword(ctx.request.body.id) 45 | ctx.body = this.success() 46 | } 47 | 48 | public async modifyPassword() { 49 | const { ctx } = this 50 | await ctx.service.system.user.modifyPassword(ctx.request.body.password) 51 | ctx.body = this.success() 52 | } 53 | 54 | public async updateUserInfo() { 55 | const { ctx } = this 56 | await ctx.service.system.user.updateUserInfo(ctx.request.body) 57 | ctx.body = this.success() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/controller/upload.ts: -------------------------------------------------------------------------------- 1 | import BaseController from './BaseController' 2 | 3 | export default class Upload extends BaseController { 4 | public async upload() { 5 | const data = await this.ctx.service.upload.upload() 6 | this.ctx.body = this.success({ 7 | data 8 | }) 9 | } 10 | 11 | public async readFile() { 12 | const { ctx } = this 13 | const { 14 | stream, 15 | filename 16 | } = await this.ctx.service.upload.readFile() 17 | ctx.attachment(filename) 18 | ctx.body = stream 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/extend/context.ts: -------------------------------------------------------------------------------- 1 | import { Application, Context } from 'egg' 2 | import errorMessage from './error' 3 | import { DEFAULT_PAGE_LIMIT } from '../settings' 4 | import datejs from '../utils/date' 5 | 6 | interface Response { 7 | data?: any; 8 | message?: string; 9 | success?: boolean; 10 | code?: string | number; 11 | count?: number; 12 | } 13 | 14 | export default { 15 | datejs, 16 | 17 | errorMsg: errorMessage, 18 | 19 | defaultPageLimit: DEFAULT_PAGE_LIMIT, 20 | 21 | /** 22 | * 判断是否为系统管理员 23 | * @param {Context} ctx Egg Context 24 | */ 25 | async isSystemManager(ctx: Context): Promise { 26 | const token = ctx.request.headers.token 27 | const { isSystemAdmin } = await ctx.app.redis.hgetall(token) 28 | return Boolean(+isSystemAdmin) 29 | }, 30 | 31 | /** 32 | * 转换请求分页页码和每页分页量 33 | * @param {Object} pagination 分页参数 34 | * @param {number} pagination.page - 当前页码 35 | * @param {string} pagination.limit - 分页量 36 | */ 37 | conversionPagination({ 38 | page = 1, 39 | limit = DEFAULT_PAGE_LIMIT 40 | }: { 41 | page: number; 42 | limit: number; 43 | }) { 44 | return { 45 | page: page - 1, 46 | limit: +limit 47 | } 48 | }, 49 | 50 | // 请求返回结构 51 | responseStruc(res: Response = {}): Response { 52 | return { 53 | data: null, 54 | success: true, 55 | count: 0, 56 | message: errorMessage.common.success.errorMsg, 57 | code: errorMessage.common.success.code, 58 | ...res 59 | } 60 | }, 61 | 62 | /** 63 | * 删除redis中保存的文件路径 64 | * @param {string} str 内容 65 | * @param {EggApplication} app egg 66 | */ 67 | async deleteFilesByReids(str: string, app: Application) { 68 | const files = str.match(/((?=(\/upload))(\S+\.\w{2,4}))+/gi) || [] 69 | for (let i = 0; i < files.length; i++) { 70 | await app.redis.lrem('files', 0, files[i]) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/extend/error/index.ts: -------------------------------------------------------------------------------- 1 | interface ErrorMessage { 2 | [key: string]: { 3 | readonly errorMsg: string; 4 | readonly code: string | number; 5 | }; 6 | } 7 | 8 | const common: ErrorMessage = { 9 | success: { 10 | errorMsg: '请求成功', 11 | code: 200 12 | }, 13 | 14 | failed: { 15 | errorMsg: '请求失败', 16 | code: 0 17 | }, 18 | 19 | notFound: { 20 | errorMsg: 'Not Found', 21 | code: 404 22 | }, 23 | 24 | serverError: { 25 | errorMsg: '服务内部错误', 26 | code: '10000' 27 | }, 28 | 29 | verificationFailed: { 30 | errorMsg: '参数校验失败', 31 | code: '10001' 32 | }, 33 | 34 | invalidToken: { 35 | errorMsg: '用户登录已过期', 36 | code: '10002' 37 | }, 38 | 39 | noAuthority: { 40 | errorMsg: '暂无请求权限', 41 | code: '10003' 42 | }, 43 | 44 | captchaError: { 45 | errorMsg: '验证码错误', 46 | code: '10004' 47 | } 48 | } 49 | 50 | const login: ErrorMessage = { 51 | noUser: { 52 | errorMsg: '用户不存在', 53 | code: '10100' 54 | }, 55 | 56 | passwordError: { 57 | errorMsg: '密码错误', 58 | code: '10101' 59 | }, 60 | 61 | authorityError: { 62 | errorMsg: '该账号暂无访问权限', 63 | code: '10102' 64 | } 65 | } 66 | 67 | const user: ErrorMessage = { 68 | registeredFailed: { 69 | errorMsg: '用户创建失败', 70 | code: '10200' 71 | }, 72 | 73 | userExists: { 74 | errorMsg: '用户已存在', 75 | code: '10201' 76 | }, 77 | 78 | userNotExists: { 79 | errorMsg: '用户不存在', 80 | code: '10202' 81 | }, 82 | 83 | noSystenAdministratorsPermission: { 84 | errorMsg: '该账号没有赋予系统管理员角色的权限', 85 | code: '10203' 86 | } 87 | } 88 | 89 | const resource: ErrorMessage = { 90 | exists: { 91 | errorMsg: '资源编码已存在', 92 | code: '10300' 93 | }, 94 | 95 | notExists: { 96 | errorMsg: '资源不存在', 97 | code: '10301' 98 | } 99 | } 100 | 101 | const role: ErrorMessage = { 102 | exists: { 103 | errorMsg: '角色已存在', 104 | code: '10400' 105 | }, 106 | 107 | notExists: { 108 | errorMsg: '角色不存在', 109 | code: '10401' 110 | } 111 | } 112 | 113 | const dictionary: ErrorMessage = { 114 | exists: { 115 | errorMsg: '字典已存在', 116 | code: '10500' 117 | }, 118 | 119 | notExists: { 120 | errorMsg: '字典不存在', 121 | code: '10501' 122 | } 123 | } 124 | 125 | export default { 126 | common, 127 | login, 128 | user, 129 | resource, 130 | role, 131 | dictionary 132 | } 133 | -------------------------------------------------------------------------------- /app/extend/helper.ts: -------------------------------------------------------------------------------- 1 | import { isObject, toUnderline } from '@xuanmo/javascript-utils' 2 | import { md5 } from '../utils/crypto' 3 | 4 | export default { 5 | isObject, 6 | md5, 7 | 8 | /** 9 | * 将对象的key如果为驼峰命名转换为下划线 10 | * @param obj 被转换的对象 11 | */ 12 | objectKeyToUnderline: (obj: object): object => { 13 | const result = {} 14 | for (const [_key, _value] of Object.entries(obj)) { 15 | result[toUnderline(_key)] = _value 16 | } 17 | return result 18 | }, 19 | 20 | /** 21 | * 一维数组转换为树形结构 22 | * @param {Array} arr 需要被转换的一维数组 23 | * @return {Array} 树形数据 24 | */ 25 | deepTree: (arr: object[]): object[] => { 26 | const deepTree = (arr: any[], parentId = 0) => { 27 | const result: object[] = [] 28 | for (let i = 0; i < arr.length; i++) { 29 | if (arr[i].parent_id === parentId) { 30 | arr[i].children = deepTree(arr, arr[i].id) 31 | result.push(arr[i]) 32 | } 33 | } 34 | return result 35 | } 36 | return deepTree(arr) 37 | }, 38 | 39 | /** 40 | * 对数组进行排序 41 | * @param {Array} arr 需要被排序的树形数组 42 | * @return {Array} 处理后的数组 43 | */ 44 | sortTreeArr: (arr: object[]): object[] => { 45 | function sortArr(arr: T[]): T[] { 46 | arr.map((item: any) => { 47 | if (item.children.length) item.children = sortArr(item.children) 48 | return item 49 | }) 50 | return arr.sort((a: any, b: any) => a.sort - b.sort) 51 | } 52 | 53 | return sortArr(arr) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/middleware/error_handler.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'egg' 2 | 3 | export default function() { 4 | return async function errorHandler(ctx: Context, next: () => Promise) { 5 | try { 6 | await next() 7 | } catch (err) { 8 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 9 | ctx.app.emit('error', err, ctx) 10 | 11 | const status = err.status || 500 12 | 13 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 14 | let error: object | string = {} 15 | 16 | if (status === 500 && ctx.app.config.env === 'prod') { 17 | error = 'Internal Server Error' 18 | } 19 | 20 | // 如果状态为200时为业务逻辑错误 21 | if (status === 200) { 22 | error = { 23 | message: err.errorMsg, 24 | code: err.code, 25 | success: false, 26 | data: null, 27 | count: 0 28 | } 29 | } 30 | 31 | ctx.body = error 32 | ctx.status = status 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/middleware/validate.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'egg' 2 | import { IGNORE_LOGIN_ROUTES } from '../settings' 3 | 4 | export default function() { 5 | return async function validate(ctx: Context, next: () => Promise) { 6 | try { 7 | const token = ctx.request.headers.token 8 | const userInfo = await ctx.app.redis.hgetall(token) 9 | 10 | if (IGNORE_LOGIN_ROUTES.findIndex(v => v.test(ctx.path)) !== -1) { 11 | return await next() 12 | } 13 | 14 | if (!token || JSON.stringify(userInfo) === '{}') { 15 | ctx.throw(200, ctx.errorMsg.common.invalidToken) 16 | } 17 | 18 | // token续期 19 | await ctx.app.redis.expire(token, ctx.app.config.base.redis.expire) 20 | 21 | const { isSystemAdmin, apis } = userInfo 22 | 23 | if (+isSystemAdmin) return await next() 24 | 25 | // 非管理员账号需要校验接口请求权限 26 | if (apis.indexOf(ctx.path) === -1) { 27 | ctx.throw(200, ctx.errorMsg.common.noAuthority) 28 | } 29 | 30 | await next() 31 | } catch (err) { 32 | ctx.logger.error('[全局拦截]', err) 33 | if (err.code === 'invalid_param') { 34 | ctx.throw(200, { 35 | errorMsg: err.errors.length >= 1 ? err.errors[0].message : ctx.errorMsg.common.verificationFailed.errorMsg, 36 | code: ctx.errorMsg.common.verificationFailed.code 37 | }) 38 | } 39 | 40 | ctx.throw(200, { 41 | errorMsg: err.errorMsg || ctx.errorMsg.common.serverError.errorMsg, 42 | code: err.code || ctx.errorMsg.common.serverError.code 43 | }) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/model/dictionary.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | 3 | export default function(app: Application) { 4 | const { BIGINT, INTEGER, STRING } = app.Sequelize 5 | const Dictionary = app.model.define('dictionary', { 6 | id: { 7 | type: INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | name: { 12 | type: STRING(50), 13 | allowNull: false 14 | }, 15 | code: { 16 | type: STRING(50), 17 | allowNull: false 18 | }, 19 | value: { 20 | type: STRING(50), 21 | allowNull: false, 22 | defaultValue: '' 23 | }, 24 | parent_id: { 25 | type: BIGINT, 26 | allowNull: false, 27 | defaultValue: 0 28 | }, 29 | sort: { 30 | type: INTEGER, 31 | allowNull: false, 32 | defaultValue: 0 33 | }, 34 | create_time: { 35 | type: BIGINT, 36 | allowNull: false 37 | }, 38 | is_deleted: { 39 | type: INTEGER, 40 | allowNull: false, 41 | defaultValue: 0 42 | } 43 | }) 44 | 45 | return class extends Dictionary { 46 | static readonly tableName = 'nkm_dictionary' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/model/resource.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | 3 | export default function(app: Application) { 4 | const { BIGINT, INTEGER, STRING } = app.Sequelize 5 | const Resource = app.model.define('resource', { 6 | id: { 7 | type: INTEGER, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | code: { 12 | type: STRING, 13 | primaryKey: true, 14 | allowNull: false 15 | }, 16 | name: { 17 | type: STRING, 18 | allowNull: false 19 | }, 20 | type: { 21 | type: STRING(50), 22 | allowNull: false 23 | }, 24 | parent_id: { 25 | type: INTEGER, 26 | allowNull: false, 27 | defaultValue: 0 28 | }, 29 | parent_code: { 30 | type: STRING, 31 | allowNull: false, 32 | defaultValue: '' 33 | }, 34 | icon: { 35 | type: STRING, 36 | allowNull: false, 37 | defaultValue: '' 38 | }, 39 | sort: { 40 | type: INTEGER, 41 | allowNull: false, 42 | defaultValue: 1 43 | }, 44 | path: { 45 | type: STRING, 46 | allowNull: false, 47 | defaultValue: '' 48 | }, 49 | enabled: { 50 | type: INTEGER, 51 | allowNull: false, 52 | defaultValue: 1 53 | }, 54 | create_time: { 55 | type: BIGINT, 56 | allowNull: false 57 | }, 58 | is_deleted: { 59 | type: INTEGER, 60 | allowNull: false, 61 | defaultValue: 0 62 | } 63 | }) 64 | 65 | return class extends Resource { 66 | static readonly tableName = 'nkm_resource' 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/model/role.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | 3 | export default function(app: Application) { 4 | const { INTEGER, STRING, BIGINT } = app.Sequelize 5 | const Role = app.model.define('role', { 6 | id: { 7 | type: INTEGER, 8 | autoIncrement: true, 9 | primaryKey: true 10 | }, 11 | name: { 12 | type: STRING, 13 | allowNull: false, 14 | defaultValue: '' 15 | }, 16 | code: { 17 | type: STRING, 18 | allowNull: false, 19 | defaultValue: '' 20 | }, 21 | permission: { 22 | type: STRING, 23 | allowNull: false, 24 | defaultValue: '' 25 | }, 26 | create_time: { 27 | type: BIGINT, 28 | allowNull: false 29 | }, 30 | is_deleted: { 31 | type: INTEGER, 32 | allowNull: false, 33 | defaultValue: 0 34 | } 35 | }) 36 | 37 | return class extends Role { 38 | static readonly tableName = 'nkm_role' 39 | 40 | static associate() { 41 | app.model.Role.belongsTo(app.model.User, { 42 | foreignKey: 'code', 43 | targetKey: 'role', 44 | as: 'u' 45 | }) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/model/user.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | import { DEFAULT_RULE_CODE } from '../settings' 3 | 4 | export default function(app: Application) { 5 | const { STRING, BIGINT, INTEGER } = app.Sequelize 6 | 7 | const User = app.model.define('users', { 8 | id: { 9 | type: INTEGER, 10 | primaryKey: true, 11 | autoIncrement: true 12 | }, 13 | login_name: { 14 | type: STRING(60), 15 | allowNull: false 16 | }, 17 | password: { 18 | type: STRING, 19 | allowNull: false 20 | }, 21 | display_name: { 22 | type: STRING(60), 23 | allowNull: false 24 | }, 25 | email: { 26 | type: STRING(100), 27 | defaultValue: '', 28 | allowNull: false 29 | }, 30 | role: { 31 | type: STRING(50), 32 | defaultValue: DEFAULT_RULE_CODE, 33 | allowNull: false 34 | }, 35 | registered_time: BIGINT, 36 | last_login_time: BIGINT, 37 | status: { 38 | type: INTEGER, 39 | defaultValue: 1 40 | }, 41 | is_system_admin: { 42 | type: INTEGER, 43 | defaultValue: 0 44 | }, 45 | avatar: { 46 | type: STRING, 47 | allowNull: false, 48 | defaultValue: '' 49 | }, 50 | agent: { 51 | type: STRING, 52 | allowNull: false, 53 | defaultValue: '' 54 | }, 55 | is_deleted: { 56 | type: INTEGER, 57 | allowNull: false, 58 | defaultValue: 0 59 | } 60 | }) 61 | 62 | return class extends User { 63 | static readonly tableName = 'nkm_users' 64 | 65 | static associate() { 66 | app.model.User.belongsTo(app.model.Role, { 67 | foreignKey: 'role', 68 | targetKey: 'code', 69 | as: 'r' 70 | }) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/router.ts: -------------------------------------------------------------------------------- 1 | import { Application } from 'egg' 2 | import { ROUTER_PREFIX } from './settings' 3 | 4 | export default (app: Application) => { 5 | const { controller, router } = app 6 | 7 | router.prefix(ROUTER_PREFIX) 8 | 9 | router.post('/login', controller.login.login) 10 | 11 | router.post('/login-out', controller.loginOut.loginOut) 12 | 13 | router.post('/upload', controller.upload.upload) 14 | router.get('/readfile', controller.upload.readFile) 15 | 16 | router.get('/system/user/list', controller.system.user.getUserList) 17 | router.post('/system/user/registered', controller.system.user.registered) 18 | router.post('/system/user/del', controller.system.user.del) 19 | router.post('/system/user/modify-status', controller.system.user.modifyStatus) 20 | router.post('/system/user/modify-role', controller.system.user.modifyRole) 21 | router.post('/system/user/reset-password', controller.system.user.resetPassword) 22 | router.post('/system/user/modify-password', controller.system.user.modifyPassword) 23 | router.post('/system/user/update-info', controller.system.user.updateUserInfo) 24 | 25 | router.post('/system/resource/save', controller.system.resource.save) 26 | router.post('/system/resource/del', controller.system.resource.del) 27 | router.get('/system/resource/tree', controller.system.resource.getTree) 28 | router.get('/system/resource/list', controller.system.resource.getList) 29 | 30 | router.get('/system/role/list', controller.system.role.getList) 31 | router.post('/system/role/save', controller.system.role.save) 32 | router.post('/system/role/del', controller.system.role.del) 33 | 34 | router.get('/system/dictionary/tree', controller.system.dictionary.getTree) 35 | router.post('/system/dictionary/save', controller.system.dictionary.save) 36 | router.post('/system/dictionary/del', controller.system.dictionary.del) 37 | 38 | router.get('/captcha', controller.captcha.init) 39 | } 40 | -------------------------------------------------------------------------------- /app/schedule/clear_file.ts: -------------------------------------------------------------------------------- 1 | import { Subscription } from 'egg' 2 | import fs = require('fs') 3 | import path = require('path') 4 | import { isEmpty } from '@xuanmo/javascript-utils' 5 | 6 | export default class ClearFile extends Subscription { 7 | static get schedule() { 8 | return { 9 | cron: '0 0 0 * * 1', 10 | type: 'all' 11 | } 12 | } 13 | 14 | async subscribe() { 15 | const { app } = this 16 | const files = await app.redis.lrange('files', 0, -1) 17 | if (isEmpty(files)) return 18 | files.forEach((item: string) => { 19 | try { 20 | fs.unlinkSync(path.join(__dirname, '../public', item)) 21 | app.redis.lrem('files', 0, item) 22 | } catch (err) { 23 | app.logger.error(`[定时任务-文件清理] ${err}`) 24 | } 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/service/captcha.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | import { createMathExpr } from 'svg-captcha' 3 | 4 | export default class Captcha extends Service { 5 | public async init() { 6 | const { ctx, app } = this 7 | const { token } = ctx.query 8 | const { data, text } = createMathExpr({ 9 | noise: 0, 10 | color: true, 11 | ignoreChars: '0o1i', 12 | width: 100, 13 | height: 30, 14 | fontSize: 36 15 | }) 16 | 17 | if (!token) { 18 | const newToken = ctx.helper.md5(Date.now() + '') 19 | ctx.cookies.set('token', newToken) 20 | await app.redis.set(`captcha:${newToken}`, text, app.config.base.redis.mode, 600) 21 | return { 22 | token: newToken, 23 | image: data 24 | } 25 | } 26 | 27 | if (token) { 28 | await app.redis.set(`captcha:${token}`, text, app.config.base.redis.mode, 600) 29 | return { 30 | token, 31 | image: data 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/service/login.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | import { NO_AUTHORIZATION_REQUIRED_ROUTES } from '../settings' 3 | import { objectKeyToCamelCase } from '@xuanmo/javascript-utils' 4 | import { AESHelper } from '../utils/crypto' 5 | 6 | export default class Login extends Service { 7 | /** 8 | * 生成用户信息 9 | * @param user 用户信息 10 | */ 11 | private async _generateUserInfo(user: { 12 | login_name: string; 13 | password: string; 14 | display_name: string; 15 | email: string; 16 | role: string; 17 | status: number; 18 | is_system_admin: number; 19 | avatar: string; 20 | }) { 21 | const { ctx, app } = this 22 | 23 | // 生成token并存入redis 24 | const token = ctx.helper.md5(user.login_name) 25 | 26 | const authority = await this._generateAuthority(user.role) 27 | 28 | delete user.password 29 | 30 | await app.redis.hmset(token, { 31 | apis: JSON.stringify(authority.apis), 32 | ...objectKeyToCamelCase(user) 33 | }) 34 | app.redis.expire(token, app.config.base.redis.expire) 35 | 36 | delete authority.apis 37 | 38 | return { 39 | token, 40 | userInfo: user, 41 | ...authority 42 | } 43 | } 44 | 45 | // 生成权限信息 46 | private async _generateAuthority(roleCode: string) { 47 | console.log(roleCode) 48 | const { ctx } = this 49 | 50 | // 查找用户对应的角色权限 51 | const roleList: any = await ctx.model.Role.findAll({ 52 | where: { 53 | code: roleCode.split(',') 54 | }, 55 | attributes: ['permission'], 56 | raw: true 57 | }) 58 | 59 | // 账号没有权限访问 60 | if (!roleList) ctx.throw(200, ctx.errorMsg.login.authorityError) 61 | 62 | // 查找所有的资源 63 | let resource: any = [] 64 | 65 | try { 66 | resource = await ctx.model.Resource.findAll({ 67 | raw: true, 68 | where: { 69 | enabled: 1, 70 | is_deleted: 0 71 | } 72 | }) 73 | } catch (e) { 74 | ctx.throw(200, ctx.errorMsg.login.authorityError) 75 | } 76 | 77 | // 当前用户的权限 78 | let currentUserPermission: string[] = [] 79 | roleList.forEach((item: { 80 | permission: string; 81 | }) => { 82 | currentUserPermission.push(...item.permission.split(',')) 83 | }) 84 | currentUserPermission = [...new Set(currentUserPermission)] 85 | 86 | const menu: object[] = [] 87 | 88 | const menuUrls: string[] = [] 89 | 90 | const btnCodes: string[] = [] 91 | 92 | const apis: string[] = [] 93 | 94 | resource.forEach((item: any) => { 95 | if (currentUserPermission.findIndex(v => +v === item.id) !== -1) { 96 | switch (item.type) { 97 | case 'system:resource:menu': 98 | menu.push(item) 99 | menuUrls.push(item.path) 100 | break 101 | case 'system:resource:page': 102 | menuUrls.push(item.path) 103 | break 104 | case 'system:resource:btn': 105 | btnCodes.push(item.code) 106 | break 107 | case 'system:resource:api': 108 | apis.push(item.path) 109 | break 110 | } 111 | } 112 | }) 113 | 114 | return { 115 | menu: ctx.helper.sortTreeArr(ctx.helper.deepTree(menu)), 116 | menuUrls, 117 | btnCodes, 118 | apis: [ 119 | ...apis, 120 | ...NO_AUTHORIZATION_REQUIRED_ROUTES 121 | ] 122 | } 123 | } 124 | 125 | // 更新最后的登录信息 126 | private async _updateLastLoginInfo(userName: string) { 127 | await this.ctx.model.User.update({ 128 | last_login_time: Date.now(), 129 | agent: this.ctx.headers['user-agent'] 130 | }, { 131 | where: { 132 | login_name: userName 133 | } 134 | }) 135 | } 136 | 137 | public async login({ 138 | loginName, 139 | password, 140 | captcha, 141 | token 142 | }: { 143 | loginName: string; 144 | password: string; 145 | captcha: string; 146 | token: string; 147 | }) { 148 | const { ctx, app } = this 149 | const _captcha = await app.redis.get(`captcha:${token}`) 150 | 151 | // 验证码错误 152 | if (captcha !== _captcha) { 153 | ctx.throw(200, ctx.errorMsg.common.captchaError) 154 | } 155 | 156 | const user: any = await ctx.model.User.findOne({ 157 | where: { 158 | login_name: loginName, 159 | status: 1 160 | }, 161 | raw: true 162 | }) 163 | 164 | // 用户不存在 165 | if (!user) { 166 | ctx.throw(200, ctx.errorMsg.login.noUser) 167 | } 168 | 169 | // 密码不正确 170 | if (AESHelper.decrypt(AESHelper.decrypt(user.password)) !== AESHelper.decrypt(password)) { 171 | ctx.throw(200, ctx.errorMsg.login.passwordError) 172 | } 173 | 174 | // 更新最后登录信息 175 | await this._updateLastLoginInfo(user.login_name) 176 | 177 | return this._generateUserInfo(user) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /app/service/login_out.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | 3 | export default class LoginOut extends Service { 4 | public async loginOut(token: string) { 5 | return this.app.redis.del(token) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/service/system/dictionary.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | 3 | interface CreateOption { 4 | name: string; 5 | code: string; 6 | value: string; 7 | parentId: number; 8 | sort: number; 9 | isDelete: number; 10 | } 11 | 12 | interface UpdateOption extends CreateOption { 13 | id: number; 14 | } 15 | 16 | interface SaveOption extends UpdateOption {} 17 | 18 | export default class Dictionary extends Service { 19 | private async _queryTheExistenceById(id: number) { 20 | const dictionary = await this.ctx.model.Dictionary.findOne({ 21 | where: { 22 | id, 23 | is_deleted: 0 24 | }, 25 | raw: true 26 | }) 27 | if (!dictionary) return this.ctx.throw(200, this.ctx.errorMsg.dictionary.notExists) 28 | } 29 | 30 | // 通过code查找是否存在 31 | private async _queryTheExistenceByCode(code: string) { 32 | const dictionary = await this.ctx.model.Dictionary.findOne({ 33 | where: { 34 | code, 35 | is_deleted: 0 36 | }, 37 | raw: true 38 | }) 39 | if (dictionary) return this.ctx.throw(200, this.ctx.errorMsg.dictionary.exists) 40 | } 41 | 42 | private async _create(option: CreateOption) { 43 | const { ctx } = this 44 | 45 | await this._queryTheExistenceByCode(option.code) 46 | 47 | return ctx.model.Dictionary.create({ 48 | ...ctx.helper.objectKeyToUnderline(option), 49 | create_time: Date.now() 50 | }) 51 | } 52 | 53 | private async _update(option: UpdateOption) { 54 | const { ctx, app } = this 55 | 56 | const dictionary = await this.ctx.model.Dictionary.findOne({ 57 | where: { 58 | id: { 59 | [app.Sequelize.Op.not]: option.id 60 | }, 61 | code: option.code, 62 | is_deleted: 0 63 | }, 64 | raw: true 65 | }) 66 | 67 | if (dictionary) return this.ctx.throw(200, this.ctx.errorMsg.dictionary.exists) 68 | 69 | return ctx.model.Dictionary.update({ 70 | ...ctx.helper.objectKeyToUnderline(option), 71 | is_deleted: 0 72 | }, { 73 | where: { 74 | id: option.id 75 | } 76 | }) 77 | } 78 | 79 | public async save(option: SaveOption) { 80 | return option.id ? this._update(option) : this._create(option) 81 | } 82 | 83 | public async getTree() { 84 | const { ctx } = this 85 | const dictionary = await ctx.model.Dictionary.findAll({ 86 | raw: true, 87 | where: { 88 | is_deleted: 0 89 | } 90 | }) 91 | const disabledIds: number[] = [] 92 | const isManager = await ctx.isSystemManager(ctx) 93 | dictionary.map((item: any) => { 94 | item.codeDisabled = true 95 | if ((item.code === 'system' || item.code.indexOf('system:resource') !== -1) && !isManager) { 96 | disabledIds.push(item.id) 97 | item.disabled = true 98 | } 99 | if (disabledIds.includes(item.parent_id)) item.disabled = true 100 | return item 101 | }) 102 | return ctx.helper.sortTreeArr(ctx.helper.deepTree(dictionary)) 103 | } 104 | 105 | public async del(id: number) { 106 | await this._queryTheExistenceById(id) 107 | 108 | return this.ctx.model.Dictionary.update({ 109 | is_deleted: 1 110 | }, { 111 | where: { 112 | id 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/service/system/resource.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | 3 | interface CreateOption { 4 | code: string; 5 | name: string; 6 | type: string; 7 | parentId: number; 8 | parentCode: string; 9 | icon: string; 10 | sort: string; 11 | path: string; 12 | enabled: number; 13 | } 14 | 15 | interface UpdateOption extends CreateOption { 16 | id: number; 17 | } 18 | 19 | interface SaveOption extends UpdateOption {} 20 | 21 | export default class Resource extends Service { 22 | // 通过id查找是否存在 23 | private async _queryTheExistenceById(id: number) { 24 | const resource = await this.ctx.model.Resource.findOne({ 25 | where: { 26 | id, 27 | is_deleted: 0 28 | }, 29 | raw: true 30 | }) 31 | if (!resource) return this.ctx.throw(200, this.ctx.errorMsg.resource.notExists) 32 | } 33 | 34 | // 通过code查找是否存在 35 | private async _queryTheExistenceByCode(code: string) { 36 | const resource = await this.ctx.model.Resource.findOne({ 37 | where: { 38 | code, 39 | is_deleted: 0 40 | }, 41 | raw: true 42 | }) 43 | if (resource) return this.ctx.throw(200, this.ctx.errorMsg.resource.exists) 44 | } 45 | 46 | private async _create(option: CreateOption) { 47 | const { ctx } = this 48 | 49 | await this._queryTheExistenceByCode(option.code) 50 | 51 | return ctx.model.Resource.create({ 52 | ...ctx.helper.objectKeyToUnderline(option), 53 | create_time: Date.now() 54 | }) 55 | } 56 | 57 | private async _update(option: UpdateOption) { 58 | const { ctx, app } = this 59 | 60 | await this._queryTheExistenceById(option.id) 61 | 62 | const resource = await this.ctx.model.Resource.findOne({ 63 | where: { 64 | id: { 65 | [app.Sequelize.Op.not]: option.id 66 | }, 67 | code: option.code, 68 | is_deleted: 0 69 | }, 70 | raw: true 71 | }) 72 | 73 | if (resource) return this.ctx.throw(200, this.ctx.errorMsg.resource.exists) 74 | 75 | return ctx.model.Resource.update({ 76 | ...ctx.helper.objectKeyToUnderline(option) 77 | }, { 78 | where: { 79 | id: option.id 80 | } 81 | }) 82 | } 83 | 84 | public async save(option: SaveOption) { 85 | return option.id ? this._update(option) : this._create(option) 86 | } 87 | 88 | public async getTree() { 89 | const { ctx } = this 90 | 91 | const resource = await ctx.model.Resource.findAll({ 92 | where: { 93 | is_deleted: 0, 94 | enabled: 1 95 | }, 96 | raw: true 97 | }) 98 | 99 | return ctx.helper.sortTreeArr(ctx.helper.deepTree(resource)) 100 | } 101 | 102 | public async getList() { 103 | const { ctx } = this 104 | 105 | return await ctx.model.Resource.findAll({ 106 | where: { 107 | is_deleted: 0 108 | }, 109 | raw: true 110 | }) 111 | } 112 | 113 | public async del(id: number) { 114 | const { ctx } = this 115 | 116 | await this._queryTheExistenceById(id) 117 | 118 | return ctx.model.Resource.update({ 119 | is_deleted: 1 120 | }, { 121 | where: { 122 | id 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /app/service/system/role.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | 3 | interface CreateOption { 4 | name: string; 5 | code: string; 6 | ids: string; 7 | } 8 | 9 | interface UpdateOption extends CreateOption { 10 | id: number; 11 | } 12 | 13 | interface SaveOption extends UpdateOption {} 14 | 15 | export default class Role extends Service { 16 | // 通过id查找是否存在 17 | private async _queryTheExistenceById(id: number) { 18 | const role = await this.ctx.model.Role.findOne({ 19 | where: { 20 | id, 21 | is_deleted: 0 22 | }, 23 | raw: true 24 | }) 25 | if (!role) return this.ctx.throw(200, this.ctx.errorMsg.role.notExists) 26 | } 27 | 28 | // 通过code查找是否存在 29 | private async _queryTheExistenceByCode(code: string) { 30 | const role = await this.ctx.model.Role.findOne({ 31 | where: { 32 | code, 33 | is_deleted: 0 34 | }, 35 | raw: true 36 | }) 37 | if (role) return this.ctx.throw(200, this.ctx.errorMsg.role.exists) 38 | } 39 | 40 | private async _create(option: CreateOption) { 41 | const { ctx } = this 42 | 43 | await this._queryTheExistenceByCode(option.code) 44 | 45 | return ctx.model.Role.create({ 46 | ...option, 47 | create_time: Date.now() 48 | }) 49 | } 50 | 51 | private async _update(option: UpdateOption) { 52 | const { ctx, app } = this 53 | 54 | // 查询角色是否已经存在 55 | const role = await this.ctx.model.Role.findOne({ 56 | where: { 57 | id: { 58 | [app.Sequelize.Op.not]: option.id 59 | }, 60 | code: option.code, 61 | is_deleted: 0 62 | }, 63 | raw: true 64 | }) 65 | 66 | if (role) return this.ctx.throw(200, this.ctx.errorMsg.role.exists) 67 | 68 | return ctx.model.Role.update({ 69 | ...ctx.helper.objectKeyToUnderline(option), 70 | permission: option.ids 71 | }, { 72 | where: { 73 | id: option.id 74 | } 75 | }) 76 | } 77 | 78 | public async getList() { 79 | const result = this.ctx.model.Role.findAll({ 80 | raw: true 81 | }) 82 | return result.map((item: any) => { 83 | item.permission = item.permission.split(',').map((id: string) => +id) 84 | return item 85 | }) 86 | } 87 | 88 | public async save(option: SaveOption) { 89 | this.ctx.validate({ 90 | name: 'string', 91 | code: 'code', 92 | ids: 'string' 93 | }) 94 | 95 | return option.id ? this._update(option) : this._create(option) 96 | } 97 | 98 | public async del(id: number) { 99 | await this._queryTheExistenceById(id) 100 | 101 | return this.ctx.model.Role.update({ 102 | is_deleted: 1 103 | }, { 104 | where: { 105 | id 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/service/system/user.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | import { DEFAULT_PASSWORD, SYSTEM_ADMINISTRATOR_CODE } from '../../settings' 3 | import { AESHelper } from '../../utils/crypto' 4 | 5 | interface UserInfo { 6 | loginName: string; 7 | password: string; 8 | displayName: string; 9 | email: string; 10 | role: string; 11 | registeredTime: number; 12 | lastLoginTime: number; 13 | status: number; 14 | isSystemAdmin: number; 15 | avatar: string; 16 | } 17 | 18 | export default class User extends Service { 19 | // 判断用户是否存在 20 | private async _judgeUser(id: T) { 21 | const { ctx } = this 22 | const user = await ctx.model.User.findOne({ 23 | where: { 24 | id 25 | } 26 | }) 27 | if (!user) ctx.throw(200, ctx.errorMsg.user.userNotExists) 28 | } 29 | 30 | // 获取所有用户 31 | public async getUserList({ 32 | page = 0, 33 | limit = 1 34 | }: { 35 | page: number; 36 | limit: number; 37 | }) { 38 | const data: any = this.ctx.model.User.findAndCountAll({ 39 | attributes: ['id', 'login_name', 'display_name', 'email', 'role', 'registered_time', 'last_login_time', 'status', 'is_system_admin', 'avatar', 'agent', 'is_deleted'], 40 | offset: page, 41 | limit, 42 | where: { 43 | is_deleted: 0 44 | } 45 | }).then((res: any) => { 46 | res.rows.map((item: any) => { 47 | item.role = item.role.split(',') 48 | return item 49 | }) 50 | return res 51 | }) 52 | return data 53 | } 54 | 55 | // 注册用户,注册功能不对外开放 56 | public async registered(userInfo: UserInfo) { 57 | const { ctx } = this 58 | 59 | // 校验 60 | ctx.validate({ 61 | loginName: 'loginName', 62 | displayName: 'name', 63 | // displayName: 'name', 64 | password: 'password', 65 | email: { 66 | type: 'email', 67 | required: false 68 | }, 69 | avatar: { 70 | type: 'string', 71 | required: false 72 | } 73 | }) 74 | 75 | const user = await ctx.model.User.findOne({ 76 | where: { 77 | login_name: userInfo.loginName 78 | } 79 | }) 80 | 81 | if (user) ctx.throw(200, ctx.errorMsg.user.userExists) 82 | 83 | return ctx.model.User.create({ 84 | ...ctx.helper.objectKeyToUnderline(userInfo), 85 | password: AESHelper.encrypt(userInfo.password), 86 | registered_time: Date.now(), 87 | last_login_time: Date.now() 88 | }) 89 | } 90 | 91 | // 删除用户 92 | public async del(id: number) { 93 | await this._judgeUser(id) 94 | 95 | return this.ctx.model.User.update({ 96 | is_deleted: 1 97 | }, { 98 | where: { 99 | id 100 | } 101 | }) 102 | } 103 | 104 | // 修改用户状态 105 | public async modifyStatus(id: number, status: number) { 106 | await this._judgeUser(id) 107 | 108 | return this.ctx.model.User.update({ 109 | status 110 | }, { 111 | where: { 112 | id 113 | } 114 | }) 115 | } 116 | 117 | // 用户修改角色 118 | public async modifyRole(id: number, role: string) { 119 | const { ctx } = this 120 | 121 | const isSystemAdmin = await ctx.isSystemManager(ctx) 122 | // 如果不是系统管理员,角色不能包含系统管理员 123 | if (!isSystemAdmin && role.indexOf(SYSTEM_ADMINISTRATOR_CODE) !== -1) { 124 | ctx.throw(200, ctx.errorMsg.user.noSystenAdministratorsPermission) 125 | } 126 | 127 | await this._judgeUser(id) 128 | 129 | return this.ctx.model.User.update({ 130 | role 131 | }, { 132 | where: { 133 | id 134 | } 135 | }) 136 | } 137 | 138 | // 重置密码 139 | public async resetPassword(id: number) { 140 | const { ctx } = this 141 | await this._judgeUser(id) 142 | 143 | return ctx.model.User.update({ 144 | password: AESHelper.encrypt(AESHelper.encrypt(DEFAULT_PASSWORD)) 145 | }, { 146 | where: { 147 | id 148 | } 149 | }) 150 | } 151 | 152 | // 修改密码 153 | public async modifyPassword(password: string) { 154 | const { ctx } = this 155 | 156 | const id = await ctx.app.redis.hget(ctx.request.headers.token, 'id') 157 | 158 | await this._judgeUser(id) 159 | 160 | return ctx.model.User.update({ 161 | password: AESHelper.encrypt(password) 162 | }, { 163 | where: { 164 | id 165 | } 166 | }) 167 | } 168 | 169 | public async updateUserInfo({ 170 | displayName, 171 | email, 172 | avatar 173 | }: { 174 | displayName: string; 175 | email: string; 176 | avatar: string; 177 | }) { 178 | const { ctx, app } = this 179 | 180 | ctx.validate({ 181 | displayName: 'name', 182 | email: 'email', 183 | avatar: 'string' 184 | }) 185 | 186 | // 从redis中删除匹配的文件路径 187 | await ctx.deleteFilesByReids(avatar, app) 188 | 189 | const id = await ctx.app.redis.hget(ctx.request.headers.token, 'id') 190 | 191 | await this._judgeUser(id) 192 | 193 | return ctx.model.User.update({ 194 | display_name: displayName, 195 | email, 196 | avatar 197 | }, { 198 | where: { 199 | id 200 | } 201 | }) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /app/service/upload.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'egg' 2 | import path = require('path') 3 | import fs = require('fs') 4 | import { ROUTER_PREFIX } from '../settings' 5 | 6 | interface FileResponse { 7 | url: string; 8 | name: string; 9 | type: string; 10 | } 11 | 12 | export default class extends Service { 13 | public async upload() { 14 | const { ctx, app } = this 15 | // 存放文件的目录 16 | const dir = path.join(__dirname, '../public/upload') 17 | 18 | const year = ctx.datejs().format('yyyy') 19 | const month = ctx.datejs().format('MM') 20 | 21 | // 文件上传类型 22 | const dirType = ['account', 'editor'] 23 | const fileType = dirType.indexOf(ctx.request.body.type) === -1 ? '' : ctx.request.body.type 24 | 25 | // 判断目录是否存在 26 | !fs.existsSync(dir) && fs.mkdirSync(dir) 27 | !fs.existsSync(`${dir}/${year}`) && fs.mkdirSync(`${dir}/${year}`) 28 | !fs.existsSync(`${dir}/${year}/${month}`) && fs.mkdirSync(`${dir}/${year}/${month}`) 29 | !fs.existsSync(`${dir}/${year}/${month}/${fileType}`) && fs.mkdirSync(`${dir}/${year}/${month}/${fileType}`) 30 | 31 | const writeDir = `${dir}/${year}/${month}/${fileType}` 32 | 33 | const files: FileResponse[] = [] 34 | const filesRelativeAddress: string[] = [] 35 | for (let i = 0; i < ctx.request.files.length; i++) { 36 | const file = ctx.request.files[i] 37 | // 创建可读流 38 | const reader = fs.createReadStream(file.filepath) 39 | 40 | // 获取上传文件扩展名 41 | const ext = file.filename.split('.').pop() 42 | 43 | // 文件名 44 | const filename = `${ctx.datejs().format('yyyyMMddHHmmss')}${Math.random().toString().substring(2, 8)}.${ext}` 45 | 46 | // 创建可写流 47 | const upStream = fs.createWriteStream(`${writeDir}/${filename}`) 48 | const relativeAddress = `/upload/${year}/${month}/${fileType}/${filename}` 49 | const remoteAddress = `${ROUTER_PREFIX}/readfile?path=${relativeAddress}` 50 | 51 | // 可读流通过管道写入可写流 52 | reader.pipe(upStream) 53 | 54 | files.push({ 55 | url: remoteAddress, 56 | type: file.mime, 57 | name: filename 58 | }) 59 | filesRelativeAddress.push(relativeAddress) 60 | } 61 | 62 | // 将文件地址保存到redis中 63 | await app.redis.rpush('files', filesRelativeAddress) 64 | 65 | return files 66 | } 67 | 68 | public async readFile() { 69 | try { 70 | const { ctx } = this 71 | const stream = fs.createReadStream(path.join(__dirname, `../public/${ctx.request.query.path}`)) 72 | return { 73 | stream, 74 | filename: stream.path.toString().replace(/([a-z\d]+\.[a-z\d]+)$/, '$1') 75 | } 76 | } catch (err) { 77 | this.ctx.logger.error('[文件读取失败]', JSON.stringify(err || {})) 78 | return { 79 | stream: null, 80 | filename: '' 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/settings.ts: -------------------------------------------------------------------------------- 1 | // 用于加密安全key 2 | export const SECRET_KEY = '@$!nkm-123456' 3 | 4 | // 路由前缀 5 | export const ROUTER_PREFIX = '/api/nkm-admin' 6 | 7 | // 新用户默认角色编码 8 | export const DEFAULT_RULE_CODE = 'test' 9 | 10 | // 默认分页量 11 | export const DEFAULT_PAGE_LIMIT = 10 12 | 13 | // 默认密码 14 | export const DEFAULT_PASSWORD = 'nkm-123456' 15 | 16 | // 系统管理员角色编码 17 | export const SYSTEM_ADMINISTRATOR_CODE = 'systemAdministrator' 18 | 19 | // 不需要登录授权的接口 20 | export const IGNORE_LOGIN_ROUTES = [ 21 | new RegExp(`${ROUTER_PREFIX}/login$`), 22 | new RegExp(`${ROUTER_PREFIX}/login-out$`), 23 | new RegExp(`${ROUTER_PREFIX}/captcha(\\?.*)?$`), 24 | new RegExp(`${ROUTER_PREFIX}/readfile(\\?.*)?$`), 25 | new RegExp(`${ROUTER_PREFIX}/system/user/registered`) 26 | ] 27 | 28 | // 无需授权的路由 29 | export const NO_AUTHORIZATION_REQUIRED_ROUTES = [ 30 | `${ROUTER_PREFIX}/upload`, 31 | `${ROUTER_PREFIX}/system/dictionary/tree`, 32 | `${ROUTER_PREFIX}/system/user/modify-password`, 33 | `${ROUTER_PREFIX}/system/user/update-info`, 34 | `${ROUTER_PREFIX}/system/resource/tree`, 35 | `${ROUTER_PREFIX}/system/role/list`, 36 | `${ROUTER_PREFIX}/tags/list`, 37 | `${ROUTER_PREFIX}/tags/save`, 38 | `${ROUTER_PREFIX}/tags/del` 39 | ] 40 | -------------------------------------------------------------------------------- /app/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from 'crypto-js' 2 | import { SECRET_KEY } from '../settings' 3 | import { isObject } from '@xuanmo/javascript-utils' 4 | 5 | export const md5 = (value: string, salt?: boolean | string) => { 6 | return CryptoJS.MD5(`${value}${SECRET_KEY}${typeof salt === 'boolean' ? Date.now() : salt ?? ''}`).toString() 7 | } 8 | 9 | class AESHandle { 10 | SECRET_KEY = md5(SECRET_KEY).toString() 11 | 12 | encrypt(data: string) { 13 | if (!data) return '' 14 | return CryptoJS.AES.encrypt(isObject(data) ? JSON.stringify(data) : data, this.SECRET_KEY).toString() 15 | } 16 | 17 | decrypt(cipherText: string) { 18 | if (!cipherText) return '' 19 | return CryptoJS.AES.decrypt(cipherText, this.SECRET_KEY).toString(CryptoJS.enc.Utf8) 20 | } 21 | } 22 | 23 | export const AESHelper: AESHandle = new AESHandle() 24 | -------------------------------------------------------------------------------- /app/utils/date.ts: -------------------------------------------------------------------------------- 1 | class DateJS { 2 | private date: Date; 3 | constructor(date: Date | number) { 4 | this.date = new Date(date) 5 | } 6 | 7 | // 小于10的填充0补位 8 | private _paddingZero(n: number) { 9 | return n < 10 ? `0${n}` : n 10 | } 11 | 12 | public format(fmt = 'yyyy-MM-dd HH:mm:ss') { 13 | const obj = { 14 | 'y+': this.date.getFullYear(), 15 | 'M{2}': this._paddingZero(this.date.getMonth() + 1), 16 | 'd{2}': this._paddingZero(this.date.getDate()), 17 | 'H{2}': this._paddingZero(this.date.getHours()), 18 | 'h{2}': this._paddingZero(this.date.getHours() % 12), 19 | 'm{2}': this._paddingZero(this.date.getMinutes()), 20 | 's{2}': this._paddingZero(this.date.getSeconds()), 21 | M: this.date.getMonth() + 1, 22 | d: this.date.getDate(), 23 | H: this.date.getHours(), 24 | h: this.date.getHours() % 12, 25 | m: this.date.getMinutes(), 26 | s: this.date.getSeconds(), 27 | W: this.date.getDay() 28 | } 29 | Object.keys(obj).forEach(key => { 30 | const regexp = new RegExp(`(${key})([^a-zA-Z])?`) 31 | if (regexp.test(fmt)) { 32 | fmt = fmt.replace(RegExp.$1, obj[key]) 33 | } 34 | }) 35 | return fmt 36 | } 37 | } 38 | 39 | const datejs = (date: Date | number = Date.now()) => new DateJS(date) 40 | 41 | export default datejs 42 | -------------------------------------------------------------------------------- /app/validator/index.ts: -------------------------------------------------------------------------------- 1 | import { AESHelper } from '../utils/crypto' 2 | 3 | export default { 4 | password: (value: string) => { 5 | const min = 6 6 | const max = 16 7 | const _value = AESHelper.decrypt(value) 8 | if (_value.length < min) return `密码长度不能少于${min}位` 9 | if (_value.length > max) return `密码长度不能大于${max}位` 10 | if (!/^[a-z\d,./!@#$*&-]+$/i.test(_value)) return '密码格式错误' 11 | }, 12 | 13 | loginName: (value: string) => { 14 | if (!/^[a-z\d]+$/i.test(value)) return '账号格式错误' 15 | }, 16 | 17 | name: (value: string) => { 18 | if (!/^[a-z\d\u2E80-\u9FFF]+$/i.test(value)) return '名称格式错误' 19 | }, 20 | 21 | email: (value: string) => { 22 | if (!/^(\w+|\w+(\.\w+))+@(\w+\.)+\w+$/.test(value)) return '邮箱格式错误' 23 | }, 24 | 25 | code: (value: string) => { 26 | if (!/^[a-z\d:-_]+$/.test(value)) return '编码格式错误' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '8' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /config/config.default.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg' 2 | 3 | export default (appInfo: EggAppInfo) => { 4 | const config = {} as PowerPartial 5 | 6 | // override config from framework / plugin 7 | // use for cookie sign key, should change to your own and keep security 8 | config.keys = appInfo.name + Date.now() 9 | 10 | // add your egg config in here 11 | config.middleware = ['errorHandler', 'validate'] 12 | 13 | config.sequelize = { 14 | username: 'root', 15 | password: '123456.x', 16 | database: 'nkm_admin', 17 | host: '127.0.0.1', 18 | timezone: '+08:00', 19 | define: { 20 | timestamps: false, 21 | freezeTableName: true, 22 | underscored: false 23 | } 24 | } 25 | 26 | config.validate = { 27 | validateRoot: true 28 | } 29 | 30 | config.logger = { 31 | outputJSON: true, 32 | dir: `${appInfo.root}/logs/${appInfo.name}` 33 | } 34 | 35 | config.multipart = { 36 | mode: 'file' 37 | } 38 | 39 | config.redis = { 40 | client: { 41 | port: 6379, 42 | host: '127.0.0.1', 43 | password: '', 44 | db: 0 45 | } 46 | } 47 | 48 | // the return config will combines to EggAppConfig 49 | return { 50 | ...config, 51 | base: { 52 | redis: { 53 | expire: 60 * 60, 54 | mode: 'EX' 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/config.local.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, PowerPartial } from 'egg' 2 | 3 | export default () => { 4 | const config: PowerPartial = {} 5 | return config 6 | } 7 | -------------------------------------------------------------------------------- /config/config.prod.ts: -------------------------------------------------------------------------------- 1 | import { EggAppConfig, PowerPartial } from 'egg' 2 | 3 | export default () => { 4 | const config: PowerPartial = {} 5 | return config 6 | } 7 | -------------------------------------------------------------------------------- /config/plugin.ts: -------------------------------------------------------------------------------- 1 | import { EggPlugin } from 'egg' 2 | 3 | const plugin: EggPlugin = { 4 | sequelize: { 5 | enable: true, 6 | package: 'egg-sequelize' 7 | }, 8 | 9 | redis: { 10 | enable: true, 11 | package: 'egg-redis' 12 | }, 13 | 14 | validate: { 15 | enable: true, 16 | package: 'egg-validate' 17 | } 18 | } 19 | 20 | export default plugin 21 | -------------------------------------------------------------------------------- /nkm-admin.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a7605be6-1b13-4c15-895e-0b25237ae18e", 4 | "name": "nkm-admin", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "用户管理", 10 | "item": [ 11 | { 12 | "name": "用户注册", 13 | "request": { 14 | "method": "POST", 15 | "header": [ 16 | { 17 | "key": "Content-Type", 18 | "name": "Content-Type", 19 | "value": "application/json", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "token", 24 | "value": "{{token}}", 25 | "type": "text" 26 | }, 27 | { 28 | "key": "x-csrf-token", 29 | "value": "{{csrfToken}}", 30 | "type": "text" 31 | } 32 | ], 33 | "body": { 34 | "mode": "raw", 35 | "raw": "{\n\t\"loginName\": \"som1214\",\n\t\"email\": \"me@xuanmo.xin\",\n\t\"displayName\": \"轩陌\",\n\t\"password\": \"123456\",\n\t\"role\": \"test\"\n}" 36 | }, 37 | "url": { 38 | "raw": "{{url}}/system/user/registered", 39 | "host": [ 40 | "{{url}}" 41 | ], 42 | "path": [ 43 | "system", 44 | "user", 45 | "registered" 46 | ] 47 | } 48 | }, 49 | "response": [] 50 | }, 51 | { 52 | "name": "用户列表", 53 | "protocolProfileBehavior": { 54 | "disableBodyPruning": true 55 | }, 56 | "request": { 57 | "method": "GET", 58 | "header": [ 59 | { 60 | "key": "token", 61 | "value": "{{token}}", 62 | "type": "text" 63 | }, 64 | { 65 | "key": "x-csrf-token", 66 | "value": "{{csrfToken}}", 67 | "type": "text" 68 | } 69 | ], 70 | "body": { 71 | "mode": "raw", 72 | "raw": "{\n\t\"page\": 1,\n\t\"limit\": 1\n}", 73 | "options": { 74 | "raw": { 75 | "language": "json" 76 | } 77 | } 78 | }, 79 | "url": { 80 | "raw": "{{url}}/system/user/list?page=1&limit=10", 81 | "host": [ 82 | "{{url}}" 83 | ], 84 | "path": [ 85 | "system", 86 | "user", 87 | "list" 88 | ], 89 | "query": [ 90 | { 91 | "key": "page", 92 | "value": "1" 93 | }, 94 | { 95 | "key": "limit", 96 | "value": "10" 97 | } 98 | ] 99 | } 100 | }, 101 | "response": [] 102 | }, 103 | { 104 | "name": "用户删除", 105 | "request": { 106 | "method": "POST", 107 | "header": [ 108 | { 109 | "key": "token", 110 | "value": "{{token}}", 111 | "type": "text" 112 | }, 113 | { 114 | "key": "Content-Type", 115 | "name": "Content-Type", 116 | "value": "application/json", 117 | "type": "text" 118 | }, 119 | { 120 | "key": "x-csrf-token", 121 | "value": "{{csrfToken}}", 122 | "type": "text" 123 | } 124 | ], 125 | "body": { 126 | "mode": "raw", 127 | "raw": "{\n\t\"id\": 222\n}" 128 | }, 129 | "url": { 130 | "raw": "{{url}}/system/user/del", 131 | "host": [ 132 | "{{url}}" 133 | ], 134 | "path": [ 135 | "system", 136 | "user", 137 | "del" 138 | ] 139 | } 140 | }, 141 | "response": [] 142 | }, 143 | { 144 | "name": "用户分配角色", 145 | "request": { 146 | "method": "POST", 147 | "header": [ 148 | { 149 | "key": "token", 150 | "value": "{{token}}", 151 | "type": "text" 152 | }, 153 | { 154 | "key": "Content-Type", 155 | "name": "Content-Type", 156 | "value": "application/json", 157 | "type": "text" 158 | }, 159 | { 160 | "key": "x-csrf-token", 161 | "value": "{{csrfToken}}", 162 | "type": "text" 163 | } 164 | ], 165 | "body": { 166 | "mode": "raw", 167 | "raw": "{\n\t\"id\": 34,\n\t\"role\": \"test123\"\n}" 168 | }, 169 | "url": { 170 | "raw": "{{url}}/system/user/modify-role", 171 | "host": [ 172 | "{{url}}" 173 | ], 174 | "path": [ 175 | "system", 176 | "user", 177 | "modify-role" 178 | ] 179 | } 180 | }, 181 | "response": [] 182 | }, 183 | { 184 | "name": "用户重置密码", 185 | "request": { 186 | "method": "POST", 187 | "header": [ 188 | { 189 | "key": "token", 190 | "value": "{{token}}", 191 | "type": "text" 192 | }, 193 | { 194 | "key": "Content-Type", 195 | "name": "Content-Type", 196 | "value": "application/json", 197 | "type": "text" 198 | }, 199 | { 200 | "key": "x-csrf-token", 201 | "value": "{{csrfToken}}", 202 | "type": "text" 203 | } 204 | ], 205 | "body": { 206 | "mode": "raw", 207 | "raw": "{\n\t\"id\": 37\n}" 208 | }, 209 | "url": { 210 | "raw": "{{url}}/system/user/reset-password", 211 | "host": [ 212 | "{{url}}" 213 | ], 214 | "path": [ 215 | "system", 216 | "user", 217 | "reset-password" 218 | ] 219 | } 220 | }, 221 | "response": [] 222 | }, 223 | { 224 | "name": "用户修改密码", 225 | "request": { 226 | "method": "POST", 227 | "header": [ 228 | { 229 | "key": "token", 230 | "value": "{{token}}", 231 | "type": "text" 232 | }, 233 | { 234 | "key": "Content-Type", 235 | "name": "Content-Type", 236 | "value": "application/json", 237 | "type": "text" 238 | }, 239 | { 240 | "key": "x-csrf-token", 241 | "value": "{{csrfToken}}", 242 | "type": "text" 243 | } 244 | ], 245 | "body": { 246 | "mode": "raw", 247 | "raw": "{\n\t\"password\": \"123456\"\n}" 248 | }, 249 | "url": { 250 | "raw": "{{url}}/system/user/modify-password", 251 | "host": [ 252 | "{{url}}" 253 | ], 254 | "path": [ 255 | "system", 256 | "user", 257 | "modify-password" 258 | ] 259 | } 260 | }, 261 | "response": [] 262 | }, 263 | { 264 | "name": "资料更新", 265 | "request": { 266 | "method": "POST", 267 | "header": [ 268 | { 269 | "key": "token", 270 | "value": "{{token}}", 271 | "type": "text" 272 | }, 273 | { 274 | "key": "x-csrf-token", 275 | "value": "{{csrfToken}}", 276 | "type": "text" 277 | } 278 | ], 279 | "body": { 280 | "mode": "raw", 281 | "raw": "{\n\t\"displayName\": \"系统管理员\"\n}", 282 | "options": { 283 | "raw": { 284 | "language": "json" 285 | } 286 | } 287 | }, 288 | "url": { 289 | "raw": "{{url}}/system/user/update-info", 290 | "host": [ 291 | "{{url}}" 292 | ], 293 | "path": [ 294 | "system", 295 | "user", 296 | "update-info" 297 | ] 298 | } 299 | }, 300 | "response": [] 301 | } 302 | ], 303 | "protocolProfileBehavior": {} 304 | }, 305 | { 306 | "name": "资源管理", 307 | "item": [ 308 | { 309 | "name": "资源保存", 310 | "request": { 311 | "method": "POST", 312 | "header": [ 313 | { 314 | "key": "token", 315 | "value": "{{token}}", 316 | "type": "text" 317 | }, 318 | { 319 | "key": "Content-Type", 320 | "name": "Content-Type", 321 | "value": "application/json", 322 | "type": "text" 323 | }, 324 | { 325 | "key": "x-csrf-token", 326 | "value": "{{csrfToken}}", 327 | "type": "text" 328 | } 329 | ], 330 | "body": { 331 | "mode": "raw", 332 | "raw": "{\n\t\"id\": 99,\n\t\"name\": \"资源管理\",\n\t\"code\": \"system:resource12\",\n\t\"type\": \"system:resource:api\",\n\t\"parentId\": 12,\n\t\"parentCode\": \"\",\n\t\"icon\": \"\",\n\t\"sort\": 1,\n\t\"url\": \"/system/resource\",\n\t\"enable\": 0\n}" 333 | }, 334 | "url": { 335 | "raw": "{{url}}/system/resource/save", 336 | "host": [ 337 | "{{url}}" 338 | ], 339 | "path": [ 340 | "system", 341 | "resource", 342 | "save" 343 | ] 344 | } 345 | }, 346 | "response": [] 347 | }, 348 | { 349 | "name": "资源列表树", 350 | "request": { 351 | "method": "GET", 352 | "header": [ 353 | { 354 | "key": "token", 355 | "value": "{{token}}", 356 | "type": "text" 357 | } 358 | ], 359 | "url": { 360 | "raw": "{{url}}/system/resource/tree", 361 | "host": [ 362 | "{{url}}" 363 | ], 364 | "path": [ 365 | "system", 366 | "resource", 367 | "tree" 368 | ] 369 | } 370 | }, 371 | "response": [] 372 | }, 373 | { 374 | "name": "资源删除", 375 | "request": { 376 | "method": "POST", 377 | "header": [ 378 | { 379 | "key": "token", 380 | "value": "{{token}}", 381 | "type": "text" 382 | }, 383 | { 384 | "key": "Content-Type", 385 | "name": "Content-Type", 386 | "value": "application/json", 387 | "type": "text" 388 | }, 389 | { 390 | "key": "x-csrf-token", 391 | "value": "{{csrfToken}}", 392 | "type": "text" 393 | } 394 | ], 395 | "body": { 396 | "mode": "raw", 397 | "raw": "{\n\t\"id\": 99\n}" 398 | }, 399 | "url": { 400 | "raw": "{{url}}/system/resource/del", 401 | "host": [ 402 | "{{url}}" 403 | ], 404 | "path": [ 405 | "system", 406 | "resource", 407 | "del" 408 | ] 409 | } 410 | }, 411 | "response": [] 412 | } 413 | ], 414 | "protocolProfileBehavior": {} 415 | }, 416 | { 417 | "name": "角色管理", 418 | "item": [ 419 | { 420 | "name": "角色保存", 421 | "request": { 422 | "method": "POST", 423 | "header": [ 424 | { 425 | "key": "token", 426 | "value": "{{token}}", 427 | "type": "text" 428 | }, 429 | { 430 | "key": "Content-Type", 431 | "name": "Content-Type", 432 | "value": "application/json", 433 | "type": "text" 434 | }, 435 | { 436 | "key": "x-csrf-token", 437 | "value": "{{csrfToken}}", 438 | "type": "text" 439 | } 440 | ], 441 | "body": { 442 | "mode": "raw", 443 | "raw": "{\n\t\"name\": \"系统管理11\",\n\t\"code\": \"test111\",\n\t\"permission\": \"11,12,1\"\n}" 444 | }, 445 | "url": { 446 | "raw": "{{url}}/system/role/save", 447 | "host": [ 448 | "{{url}}" 449 | ], 450 | "path": [ 451 | "system", 452 | "role", 453 | "save" 454 | ] 455 | } 456 | }, 457 | "response": [] 458 | }, 459 | { 460 | "name": "角色列表", 461 | "request": { 462 | "method": "GET", 463 | "header": [ 464 | { 465 | "key": "token", 466 | "value": "{{token}}", 467 | "type": "text" 468 | }, 469 | { 470 | "key": "x-csrf-token", 471 | "value": "{{csrfToken}}", 472 | "type": "text" 473 | } 474 | ], 475 | "url": { 476 | "raw": "{{url}}/system/role/list", 477 | "host": [ 478 | "{{url}}" 479 | ], 480 | "path": [ 481 | "system", 482 | "role", 483 | "list" 484 | ] 485 | } 486 | }, 487 | "response": [] 488 | }, 489 | { 490 | "name": "角色删除", 491 | "request": { 492 | "method": "POST", 493 | "header": [ 494 | { 495 | "key": "token", 496 | "value": "{{token}}", 497 | "type": "text" 498 | }, 499 | { 500 | "key": "Content-Type", 501 | "name": "Content-Type", 502 | "value": "application/json", 503 | "type": "text" 504 | }, 505 | { 506 | "key": "x-csrf-token", 507 | "value": "{{csrfToken}}", 508 | "type": "text" 509 | } 510 | ], 511 | "body": { 512 | "mode": "raw", 513 | "raw": "{\n\t\"id\": 171\n}" 514 | }, 515 | "url": { 516 | "raw": "{{url}}/system/role/del", 517 | "host": [ 518 | "{{url}}" 519 | ], 520 | "path": [ 521 | "system", 522 | "role", 523 | "del" 524 | ] 525 | } 526 | }, 527 | "response": [] 528 | } 529 | ], 530 | "protocolProfileBehavior": {} 531 | }, 532 | { 533 | "name": "数据字典", 534 | "item": [ 535 | { 536 | "name": "保存", 537 | "request": { 538 | "method": "POST", 539 | "header": [ 540 | { 541 | "key": "token", 542 | "value": "{{token}}", 543 | "type": "text" 544 | }, 545 | { 546 | "key": "Content-Type", 547 | "name": "Content-Type", 548 | "value": "application/json", 549 | "type": "text" 550 | }, 551 | { 552 | "key": "x-csrf-token", 553 | "value": "{{csrfToken}}", 554 | "type": "text" 555 | } 556 | ], 557 | "body": { 558 | "mode": "raw", 559 | "raw": "{\n\t\"id\": 23,\n\t\"name\": \"支出类型11\",\n\t\"code\": \"type\",\n\t\"value\": 1,\n\t\"parent_id\": 3\n}" 560 | }, 561 | "url": { 562 | "raw": "{{url}}/system/dictionary/save", 563 | "host": [ 564 | "{{url}}" 565 | ], 566 | "path": [ 567 | "system", 568 | "dictionary", 569 | "save" 570 | ] 571 | } 572 | }, 573 | "response": [] 574 | }, 575 | { 576 | "name": "列表树", 577 | "request": { 578 | "method": "GET", 579 | "header": [ 580 | { 581 | "key": "token", 582 | "value": "{{token}}", 583 | "type": "text" 584 | } 585 | ], 586 | "url": { 587 | "raw": "{{url}}/system/dictionary/tree", 588 | "host": [ 589 | "{{url}}" 590 | ], 591 | "path": [ 592 | "system", 593 | "dictionary", 594 | "tree" 595 | ] 596 | } 597 | }, 598 | "response": [] 599 | }, 600 | { 601 | "name": "删除", 602 | "request": { 603 | "method": "POST", 604 | "header": [ 605 | { 606 | "key": "token", 607 | "value": "{{token}}", 608 | "type": "text" 609 | }, 610 | { 611 | "key": "Content-Type", 612 | "name": "Content-Type", 613 | "value": "application/json", 614 | "type": "text" 615 | }, 616 | { 617 | "key": "x-csrf-token", 618 | "value": "{{csrfToken}}", 619 | "type": "text" 620 | } 621 | ], 622 | "body": { 623 | "mode": "raw", 624 | "raw": "{\n\t\"id\": 21\n}" 625 | }, 626 | "url": { 627 | "raw": "{{url}}/system/dictionary/del", 628 | "host": [ 629 | "{{url}}" 630 | ], 631 | "path": [ 632 | "system", 633 | "dictionary", 634 | "del" 635 | ] 636 | } 637 | }, 638 | "response": [] 639 | } 640 | ], 641 | "protocolProfileBehavior": {} 642 | }, 643 | { 644 | "name": "登录", 645 | "request": { 646 | "method": "POST", 647 | "header": [ 648 | { 649 | "key": "Content-Type", 650 | "name": "Content-Type", 651 | "value": "application/json", 652 | "type": "text" 653 | }, 654 | { 655 | "key": "x-csrf-token", 656 | "value": "{{csrfToken}}", 657 | "type": "text" 658 | } 659 | ], 660 | "body": { 661 | "mode": "raw", 662 | "raw": "{\n\t\"loginName\": \"admin\",\n\t\"password\": \"123456\",\n\t\"captcha\": \"1\",\n\t\"token\": \"14a66bfcaa258152518e2ac576ee8618\"\n}" 663 | }, 664 | "url": { 665 | "raw": "{{url}}/login", 666 | "host": [ 667 | "{{url}}" 668 | ], 669 | "path": [ 670 | "login" 671 | ] 672 | } 673 | }, 674 | "response": [] 675 | }, 676 | { 677 | "name": "登出", 678 | "request": { 679 | "method": "POST", 680 | "header": [ 681 | { 682 | "key": "token", 683 | "value": "{{token}}", 684 | "type": "text" 685 | }, 686 | { 687 | "key": "Content-Type", 688 | "name": "Content-Type", 689 | "value": "application/json", 690 | "type": "text" 691 | }, 692 | { 693 | "key": "x-csrf-token", 694 | "value": "{{csrfToken}}", 695 | "type": "text" 696 | } 697 | ], 698 | "body": { 699 | "mode": "raw", 700 | "raw": "{}" 701 | }, 702 | "url": { 703 | "raw": "{{url}}/login-out", 704 | "host": [ 705 | "{{url}}" 706 | ], 707 | "path": [ 708 | "login-out" 709 | ] 710 | } 711 | }, 712 | "response": [] 713 | }, 714 | { 715 | "name": "文件上传", 716 | "request": { 717 | "method": "POST", 718 | "header": [ 719 | { 720 | "key": "token", 721 | "value": "{{token}}", 722 | "type": "text" 723 | }, 724 | { 725 | "key": "x-csrf-token", 726 | "value": "{{csrfToken}}", 727 | "type": "text" 728 | } 729 | ], 730 | "body": { 731 | "mode": "formdata", 732 | "formdata": [ 733 | { 734 | "key": "files", 735 | "type": "file", 736 | "src": "/Users/xuanmo/Desktop/MyDocuments/素材/水果/Fruit-8.png" 737 | }, 738 | { 739 | "key": "type", 740 | "value": "account1", 741 | "type": "text" 742 | } 743 | ] 744 | }, 745 | "url": { 746 | "raw": "{{url}}/upload", 747 | "host": [ 748 | "{{url}}" 749 | ], 750 | "path": [ 751 | "upload" 752 | ] 753 | } 754 | }, 755 | "response": [] 756 | }, 757 | { 758 | "name": "验证码", 759 | "request": { 760 | "method": "GET", 761 | "header": [], 762 | "url": { 763 | "raw": "{{url}}/captcha", 764 | "host": [ 765 | "{{url}}" 766 | ], 767 | "path": [ 768 | "captcha" 769 | ] 770 | } 771 | }, 772 | "response": [] 773 | }, 774 | { 775 | "name": "文件读取", 776 | "request": { 777 | "method": "GET", 778 | "header": [ 779 | { 780 | "key": "token", 781 | "value": "{{token}}", 782 | "type": "text", 783 | "disabled": true 784 | } 785 | ], 786 | "url": { 787 | "raw": "{{url}}/readfile?path=/upload/2020/06/20200616141226631416.png", 788 | "host": [ 789 | "{{url}}" 790 | ], 791 | "path": [ 792 | "readfile" 793 | ], 794 | "query": [ 795 | { 796 | "key": "path", 797 | "value": "/upload/2020/06/20200616141226631416.png" 798 | } 799 | ] 800 | } 801 | }, 802 | "response": [] 803 | } 804 | ], 805 | "event": [ 806 | { 807 | "listen": "prerequest", 808 | "script": { 809 | "id": "2ed3459b-3b65-4359-84cc-34bc94fbcc9c", 810 | "type": "text/javascript", 811 | "exec": [ 812 | "" 813 | ] 814 | } 815 | }, 816 | { 817 | "listen": "test", 818 | "script": { 819 | "id": "1c0095c5-686d-48c5-9545-2d3e43bd94a7", 820 | "type": "text/javascript", 821 | "exec": [ 822 | "" 823 | ] 824 | } 825 | } 826 | ], 827 | "variable": [ 828 | { 829 | "id": "ec1ffd8f-c2c4-4cb5-a9c7-cd204d539904", 830 | "key": "url", 831 | "value": "", 832 | "type": "string" 833 | }, 834 | { 835 | "id": "8b7d0e39-1ee4-4823-b2ef-a11581574d4f", 836 | "key": "token", 837 | "value": "", 838 | "type": "string" 839 | }, 840 | { 841 | "id": "0b488cf9-2c6c-481b-91aa-14132e66bf48", 842 | "key": "csrfToken", 843 | "value": "", 844 | "type": "string" 845 | } 846 | ], 847 | "protocolProfileBehavior": {} 848 | } -------------------------------------------------------------------------------- /nkm_admin.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : localhost_3306 5 | Source Server Type : MySQL 6 | Source Server Version : 80027 7 | Source Host : localhost:3306 8 | Source Schema : nkm_admin 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 80027 12 | File Encoding : 65001 13 | 14 | Date: 24/04/2022 13:29:20 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for nkm_dictionary 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `nkm_dictionary`; 24 | CREATE TABLE `nkm_dictionary` ( 25 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 26 | `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 27 | `code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 28 | `value` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 29 | `parent_id` bigint unsigned NOT NULL, 30 | `sort` int unsigned NOT NULL, 31 | `create_time` bigint unsigned NOT NULL, 32 | `is_deleted` int unsigned NOT NULL DEFAULT '0' COMMENT '0:未删除 1:已删除', 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3; 35 | 36 | -- ---------------------------- 37 | -- Records of nkm_dictionary 38 | -- ---------------------------- 39 | BEGIN; 40 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (1, '系统管理', 'system', '', 0, 0, 1591973806775, 0); 41 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (2, '资源类型', 'system:resource:type', '', 1, 0, 1591973869522, 0); 42 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (3, '菜单', 'system:resource:menu', '', 2, 1, 1591973899050, 0); 43 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (4, '页面', 'system:resource:page', '', 2, 1, 1591973914964, 0); 44 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (5, '按钮', 'system:resource:btn', '', 2, 2, 1591973929488, 0); 45 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (6, '接口', 'system:resource:api', '', 2, 3, 1591973943052, 0); 46 | INSERT INTO `nkm_dictionary` (`id`, `name`, `code`, `value`, `parent_id`, `sort`, `create_time`, `is_deleted`) VALUES (7, '占位', 'placeholder', '', 2, 0, 1650778100554, 0); 47 | COMMIT; 48 | 49 | -- ---------------------------- 50 | -- Table structure for nkm_resource 51 | -- ---------------------------- 52 | DROP TABLE IF EXISTS `nkm_resource`; 53 | CREATE TABLE `nkm_resource` ( 54 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 55 | `code` varchar(200) NOT NULL, 56 | `name` varchar(255) NOT NULL, 57 | `type` varchar(50) NOT NULL, 58 | `parent_id` bigint unsigned NOT NULL, 59 | `parent_code` varchar(200) NOT NULL, 60 | `icon` varchar(200) NOT NULL, 61 | `sort` int unsigned NOT NULL, 62 | `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 63 | `enabled` int unsigned NOT NULL COMMENT '0:禁用 1:启用', 64 | `create_time` bigint unsigned NOT NULL, 65 | `is_deleted` tinyint unsigned NOT NULL DEFAULT '0', 66 | PRIMARY KEY (`id`) USING BTREE 67 | ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb3; 68 | 69 | -- ---------------------------- 70 | -- Records of nkm_resource 71 | -- ---------------------------- 72 | BEGIN; 73 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (1, 'system', '系统管理', 'system:resource:menu', 0, '', 'el-icon-setting', 2, '/system', 1, 1566128640252, 0); 74 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (2, 'system:user', '用户管理', 'system:resource:menu', 1, '', 'x-icon-users', 1, '/system/user', 1, 1566128769341, 0); 75 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (3, 'system:resource', '资源管理', 'system:resource:menu', 1, '', 'el-icon-collection', 2, '/system/resource', 1, 1566128821691, 0); 76 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (4, 'system:role', '角色管理', 'system:resource:menu', 1, '', 'x-icon-roles', 3, '/system/role', 1, 1566128846696, 0); 77 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (5, 'dashboard', '仪表盘', 'system:resource:menu', 0, '', 'el-icon-odometer', 0, '/dashboard', 1, 1566136292367, 0); 78 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (6, 'personal:center', '个人中心', 'system:resource:menu', 0, '', 'el-icon-user', 1, '/personal-center', 1, 1566220196928, 0); 79 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (7, 'system:dictionary', '数据字典', 'system:resource:menu', 1, '', 'el-icon-notebook-1', 3, '/system/dictionary', 1, 1567432900204, 0); 80 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (8, 'system:user:api:list', '列表', 'system:resource:api', 2, '', '', 0, '/api/nkm-admin/system/user/list', 1, 1592301197531, 0); 81 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (9, 'system:user:api:registered', '用户注册', 'system:resource:api', 2, '', '', 0, '/api/nkm-admin/system/user/registered', 1, 1592301311815, 0); 82 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (10, 'system:user:api:del', '用户删除', 'system:resource:api', 2, '', '', 0, '/api/nkm-admin/system/user/del', 1, 1592301366303, 0); 83 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (11, 'system:user:api:modify-role', '修改角色', 'system:resource:api', 2, '', '', 0, '/api/nkm-admin/system/user/modify-role', 1, 1592313728482, 0); 84 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (12, 'system:user:api:reset-pwd', '重置密码', 'system:resource:api', 2, '', '', 0, '/api/nkm-admin/system/user/reset-password', 1, 1592313846476, 0); 85 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (13, 'system:resource:api:save', '资源保存', 'system:resource:api', 3, '', '', 0, '/api/nkm-admin/system/resource/save', 1, 1592314794775, 0); 86 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (14, 'system:resource:api:del', '资源删除', 'system:resource:api', 3, '', '', 0, '/api/nkm-admin/system/resource/del', 1, 1592314948240, 0); 87 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (15, 'system:role:api:save', '角色保存', 'system:resource:api', 4, '', '', 0, '/api/nkm-admin/system/role/save', 1, 1592315164331, 0); 88 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (16, 'system:role:api:del', '角色删除', 'system:resource:api', 4, '', '', 0, '/api/nkm-admin/system/role/del', 1, 1592315817575, 0); 89 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (17, 'system:dict:api:save', '字典保存', 'system:resource:api', 7, '', '', 0, '/api/nkm-admin/system/dictionary/save', 1, 1592315866661, 0); 90 | INSERT INTO `nkm_resource` (`id`, `code`, `name`, `type`, `parent_id`, `parent_code`, `icon`, `sort`, `path`, `enabled`, `create_time`, `is_deleted`) VALUES (18, 'system:dict:api:del', '字典删除', 'system:resource:api', 7, '', '', 0, '/api/nkm-admin/system/dictionary/del', 1, 1592315899190, 0); 91 | COMMIT; 92 | 93 | -- ---------------------------- 94 | -- Table structure for nkm_role 95 | -- ---------------------------- 96 | DROP TABLE IF EXISTS `nkm_role`; 97 | CREATE TABLE `nkm_role` ( 98 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 99 | `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 100 | `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 101 | `permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 102 | `create_time` bigint unsigned NOT NULL, 103 | `is_deleted` int unsigned NOT NULL DEFAULT '0', 104 | PRIMARY KEY (`id`) 105 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3; 106 | 107 | -- ---------------------------- 108 | -- Records of nkm_role 109 | -- ---------------------------- 110 | BEGIN; 111 | INSERT INTO `nkm_role` (`id`, `name`, `code`, `permission`, `create_time`, `is_deleted`) VALUES (1, '系统管理员', 'systemAdministrator', '5,6,1,2,8,9,10,11,12,3,13,14,4,15,16,7,17,18', 1565586505970, 0); 112 | INSERT INTO `nkm_role` (`id`, `name`, `code`, `permission`, `create_time`, `is_deleted`) VALUES (2, '测试', 'test', '5,6,1,2,3,4,7', 1565586505970, 0); 113 | INSERT INTO `nkm_role` (`id`, `name`, `code`, `permission`, `create_time`, `is_deleted`) VALUES (3, '测试管理员', 'testAdmin', '5,6,1,2,8,9,10,11,12,3,13,14,4,15,16,7,17,18', 1592377309989, 0); 114 | COMMIT; 115 | 116 | -- ---------------------------- 117 | -- Table structure for nkm_users 118 | -- ---------------------------- 119 | DROP TABLE IF EXISTS `nkm_users`; 120 | CREATE TABLE `nkm_users` ( 121 | `id` bigint unsigned NOT NULL AUTO_INCREMENT, 122 | `login_name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', 123 | `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', 124 | `display_name` varchar(60) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', 125 | `email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '', 126 | `role` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 127 | `registered_time` bigint NOT NULL DEFAULT '1565257063368', 128 | `last_login_time` bigint NOT NULL DEFAULT '1565257063368', 129 | `status` int unsigned NOT NULL DEFAULT '1' COMMENT '0:禁用;1:启用', 130 | `is_system_admin` int unsigned NOT NULL DEFAULT '0', 131 | `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 132 | `agent` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 133 | `is_deleted` int unsigned DEFAULT '0', 134 | PRIMARY KEY (`id`) USING BTREE 135 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3; 136 | 137 | -- ---------------------------- 138 | -- Records of nkm_users 139 | -- ---------------------------- 140 | BEGIN; 141 | INSERT INTO `nkm_users` (`id`, `login_name`, `password`, `display_name`, `email`, `role`, `registered_time`, `last_login_time`, `status`, `is_system_admin`, `avatar`, `agent`, `is_deleted`) VALUES (1, 'admin', 'U2FsdGVkX1/nkZ53+gUWe+lG/z4SFcLr+d5Dik8W7hczuIDTb4tWJb6jaWDHYaBnwrsqCrS7YJ4ThNc89xI8Hg==', '系统管理员', 'me@example.com', 'systemAdministrator', 1565758490904, 1650777473467, 1, 1, '/api/nkm-admin/readfile?path=/upload/2020/06/account/20200628130018776265.png', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', 0); 142 | INSERT INTO `nkm_users` (`id`, `login_name`, `password`, `display_name`, `email`, `role`, `registered_time`, `last_login_time`, `status`, `is_system_admin`, `avatar`, `agent`, `is_deleted`) VALUES (2, 'test', 'U2FsdGVkX1+VqcaYKflrT2jtgkJ/XcHVsOVf3ynTSCkQKrmY6Ao/z3qQA8nInOR16mMa/i+8450XkqRp7x/iMw==', '测试人员1', 'm@example.com', 'test', 1592184900031, 1618410029571, 1, 0, '/api/nkm-admin/readfile?path=/upload/2020/06/account/20200616171102099796.png', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36', 0); 143 | INSERT INTO `nkm_users` (`id`, `login_name`, `password`, `display_name`, `email`, `role`, `registered_time`, `last_login_time`, `status`, `is_system_admin`, `avatar`, `agent`, `is_deleted`) VALUES (3, 'testAdmin', 'U2FsdGVkX1+slLT6JQ5iH5K2+5eia7BuYJaogrJ3CqVY2gmctVds0MJ4SWqfDvLKZSM/7WlS99pvgmHgTrEp1A==', '测试管理员', 'me@example.com', 'testAdmin', 1592378282544, 1650718890572, 1, 0, '/img/Fruit-1.ec29dc10.png', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', 0); 144 | INSERT INTO `nkm_users` (`id`, `login_name`, `password`, `display_name`, `email`, `role`, `registered_time`, `last_login_time`, `status`, `is_system_admin`, `avatar`, `agent`, `is_deleted`) VALUES (4, 'test1', 'U2FsdGVkX187NcE3grth8DROkMrajEkG1qSigjPQ/oOzxjL4Kh1MBMJM2zkDGuc3G4z0P3I40V1sdGV4MV2FxQ==', 'test1', 'me@example.com', 'test', 1618410117679, 1618410141367, 1, 0, '/img/Fruit-1.ec29dc10.png', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36', 0); 145 | COMMIT; 146 | 147 | SET FOREIGN_KEY_CHECKS = 1; 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nkm-server-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "egg": { 7 | "typescript": true, 8 | "declarations": true 9 | }, 10 | "scripts": { 11 | "bootstrap:test": "yarn --registry=https://registry.npmmirror.com/ && yarn tsc && yarn start", 12 | "start": "egg-scripts start --daemon --title=egg-server-nkm-server-ts --port=3100", 13 | "stop": "egg-scripts stop --title=egg-server-nkm-server-ts", 14 | "dev": "egg-bin dev", 15 | "debug": "egg-bin debug", 16 | "test-local": "egg-bin test", 17 | "test": "npm run lint -- --fix && npm run test-local", 18 | "cov": "egg-bin cov", 19 | "tsc": "ets && tsc -p tsconfig.json", 20 | "ci": "npm run lint && npm run cov && npm run tsc", 21 | "autod": "autod", 22 | "lint": "eslint . --ext .ts --fix", 23 | "clean": "ets clean", 24 | "push": "git push && git push gitlab master" 25 | }, 26 | "dependencies": { 27 | "@xuanmo/javascript-utils": "^0.0.22", 28 | "crypto-js": "^4.1.1", 29 | "egg": "^2.29.1", 30 | "egg-redis": "^2.4.0", 31 | "egg-scripts": "^2.6.0", 32 | "egg-sequelize": "^5.2.1", 33 | "egg-validate": "^2.0.2", 34 | "mysql2": "^2.1.0", 35 | "svg-captcha": "^1.4.0" 36 | }, 37 | "devDependencies": { 38 | "@types/mocha": "^2.2.40", 39 | "@types/node": "^7.0.12", 40 | "@types/supertest": "^2.0.0", 41 | "autod": "^3.0.1", 42 | "autod-egg": "^1.1.0", 43 | "egg-bin": "^4.11.0", 44 | "egg-ci": "^1.8.0", 45 | "egg-mock": "^3.16.0", 46 | "eslint": "^6.7.2", 47 | "eslint-config-egg": "^8.0.0", 48 | "husky": "^4.2.5", 49 | "tslib": "^1.9.0", 50 | "typescript": "^3.0.0" 51 | }, 52 | "engines": { 53 | "node": ">=8.9.0" 54 | }, 55 | "ci": { 56 | "version": "8" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "" 61 | }, 62 | "eslintIgnore": [ 63 | "coverage" 64 | ], 65 | "author": "", 66 | "license": "MIT", 67 | "husky": { 68 | "hooks": { 69 | "pre-commit": "npm run lint" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/app/controller/home.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { app } from 'egg-mock/bootstrap' 3 | 4 | describe('test/app/controller/home.test.ts', () => { 5 | it('should GET /', async () => { 6 | const result = await app.httpRequest().get('/').expect(200) 7 | assert(result.text === 'hi, egg') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/app/service/Test.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { Context } from 'egg' 3 | import { app } from 'egg-mock/bootstrap' 4 | 5 | describe('test/app/service/Test.test.js', () => { 6 | let ctx: Context 7 | 8 | before(async () => { 9 | ctx = app.mockContext() 10 | }) 11 | 12 | it('sayHi', async () => { 13 | const result = await ctx.service.test.sayHi('egg') 14 | assert(result === 'hi, egg') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "commonjs", 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "charset": "utf8", 11 | "allowJs": false, 12 | "pretty": true, 13 | "noEmitOnError": false, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "allowUnreachableCode": false, 17 | "allowUnusedLabels": false, 18 | "strictPropertyInitialization": false, 19 | "noFallthroughCasesInSwitch": true, 20 | "skipLibCheck": true, 21 | "skipDefaultLibCheck": true, 22 | "inlineSourceMap": true, 23 | "importHelpers": true 24 | }, 25 | "exclude": [ 26 | "app/public", 27 | "app/views", 28 | "node_modules*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IncomingHttpHeaders } from 'http' 2 | 3 | interface HttpHeaders extends IncomingHttpHeaders { 4 | token: string; 5 | } 6 | 7 | declare module 'egg' { 8 | interface Context { 9 | headers: HttpHeaders; 10 | } 11 | 12 | interface Request { 13 | headers: HttpHeaders; 14 | } 15 | } 16 | --------------------------------------------------------------------------------