├── .eslintignore ├── app ├── extend │ ├── note.rec │ ├── helper.js │ └── RESTfulHTTPStatus.rec ├── public │ └── attachment │ │ ├── audio.png │ │ ├── video.png │ │ └── document.png ├── controller │ ├── home.js │ ├── role.js │ ├── user.js │ ├── userAccess.js │ └── upload.js ├── model │ ├── attachment.js │ ├── role.js │ └── user.js ├── service │ ├── actionToken.js │ ├── userAccess.js │ ├── role.js │ ├── user.js │ └── upload.js ├── middleware │ └── error_handler.js └── router.js ├── .travis.yml ├── .gitignore ├── appveyor.yml ├── config ├── plugin.js └── config.default.js ├── .autod.conf.js ├── test └── app │ └── controller │ └── home.test.js ├── .eslintrc ├── README.zh-CN.md ├── README.md ├── .vscode ├── launch.json └── settings.json └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /app/extend/note.rec: -------------------------------------------------------------------------------- 1 | ctx.state.user jwt验证通过,可以获取到之前加密的data,如获取到该token对应的user -------------------------------------------------------------------------------- /app/public/attachment/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimi-block/egg-RESTfulAPI/HEAD/app/public/attachment/audio.png -------------------------------------------------------------------------------- /app/public/attachment/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimi-block/egg-RESTfulAPI/HEAD/app/public/attachment/video.png -------------------------------------------------------------------------------- /app/public/attachment/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heimi-block/egg-RESTfulAPI/HEAD/app/public/attachment/document.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '8' 5 | install: 6 | - npm i npminstall && npminstall 7 | script: 8 | - npm run ci 9 | after_script: 10 | - npminstall codecov && codecov 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | #.vscode/ 14 | app/public/uploads/ 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment') 2 | 3 | // 格式化时间 4 | exports.formatTime = time => moment(time).format('YYYY-MM-DD HH:mm:ss') 5 | 6 | // 处理成功响应 7 | exports.success = ({ ctx, res = null, msg = '请求成功' })=> { 8 | ctx.body = { 9 | code: 0, 10 | data: res, 11 | msg 12 | } 13 | ctx.status = 200 14 | } 15 | -------------------------------------------------------------------------------- /app/controller/home.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | 3 | class HomeController extends Controller { 4 | async index() { 5 | this.ctx.body = `hi, egg-RESTfulAPI! 6 | A optimized Node.js RESTful API Server Template based on egg.js. 7 | https://github.com/icxcat/egg-RESTfulAPI.git` 8 | } 9 | } 10 | 11 | module.exports = HomeController 12 | -------------------------------------------------------------------------------- /app/model/attachment.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const mongoose = app.mongoose 3 | 4 | const AttachmentSchema = new mongoose.Schema({ 5 | extname: { type: String }, 6 | url: { type: String }, 7 | filename: { type: String }, 8 | extra: { type: String }, 9 | createdAt: { type: Date, default: Date.now } 10 | }) 11 | 12 | return mongoose.model('Attachment', AttachmentSchema) 13 | 14 | } -------------------------------------------------------------------------------- /app/model/role.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const mongoose = app.mongoose 3 | 4 | const RoleSchema = new mongoose.Schema({ 5 | name: { type: String, unique: true, required: true }, 6 | access: { type: String, required: true, default: 'user' }, 7 | extra: { type: mongoose.Schema.Types.Mixed }, 8 | createdAt: { type: Date, default: Date.now } 9 | }) 10 | 11 | return mongoose.model('Role', RoleSchema) 12 | } -------------------------------------------------------------------------------- /app/service/actionToken.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Service = require('egg').Service 4 | 5 | class ActionTokenService extends Service { 6 | async apply(_id) { 7 | const {ctx} = this 8 | return ctx.app.jwt.sign({ 9 | data: { 10 | _id: _id 11 | }, 12 | exp: Math.floor(Date.now() / 1000) + (60 * 60 * 24 * 7) 13 | }, ctx.app.config.jwt.secret) 14 | } 15 | } 16 | 17 | module.exports = ActionTokenService 18 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | // had enabled by egg 2 | // exports.static = true; 3 | exports.validate = { 4 | enable: true, 5 | package: 'egg-validate', 6 | } 7 | 8 | exports.bcrypt = { 9 | enable: true, 10 | package: 'egg-bcrypt' 11 | } 12 | 13 | exports.mongoose = { 14 | enable: true, 15 | package: 'egg-mongoose', 16 | } 17 | 18 | exports.jwt = { 19 | enable: true, 20 | package: 'egg-jwt', 21 | } 22 | 23 | exports.cors = { 24 | enable: true, 25 | package: 'egg-cors', 26 | } -------------------------------------------------------------------------------- /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | 'webstorm-disable-index', 24 | ], 25 | exclude: [ 26 | './test/fixtures', 27 | './dist', 28 | ], 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | 5 | describe('test/app/controller/home.test.js', () => { 6 | 7 | it('should assert', function* () { 8 | const pkg = require('../../../package.json'); 9 | assert(app.config.keys.startsWith(pkg.name)); 10 | 11 | // const ctx = app.mockContext({}); 12 | // yield ctx.service.xx(); 13 | }); 14 | 15 | it('should GET /', () => { 16 | return app.httpRequest() 17 | .get('/') 18 | .expect('hi, egg') 19 | .expect(200); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module", 9 | "ecmaVersion": 8 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "never" 27 | ], 28 | "no-console": "off", 29 | "no-self-assign": "off", 30 | "no-unused-vars": "off" 31 | } 32 | } -------------------------------------------------------------------------------- /app/model/user.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | const mongoose = app.mongoose 3 | const UserSchema = new mongoose.Schema({ 4 | mobile: { type: String, unique: true, required: true }, 5 | password: { type: String, required: true }, 6 | realName: { type: String, required: true }, 7 | role: { type: mongoose.Schema.Types.ObjectId, ref: 'Role' }, 8 | avatar: { type: String, default: 'https://1.gravatar.com/avatar/a3e54af3cb6e157e496ae430aed4f4a3?s=96&d=mm'}, 9 | extra: { type: mongoose.Schema.Types.Mixed }, 10 | createdAt: { type: Date, default: Date.now } 11 | }) 12 | return mongoose.model('User', UserSchema) 13 | } 14 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # egg-RESTfulAPI 2 | 3 | A optimized Node.js RESTful API Server Template based on egg.js! 4 | 5 | ## 快速入门 6 | 7 | 8 | 9 | 如需进一步了解,参见 [egg 文档][egg]。 10 | 11 | ### 本地开发 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7001/ 17 | ``` 18 | 19 | ### 部署 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### 单元测试 27 | 28 | - [egg-bin] 内置了 [mocha], [thunk-mocha], [power-assert], [istanbul] 等框架,让你可以专注于写单元测试,无需理会配套工具。 29 | - 断言库非常推荐使用 [power-assert]。 30 | - 具体参见 [egg 文档 - 单元测试](https://eggjs.org/zh-cn/core/unittest)。 31 | 32 | ### 内置指令 33 | 34 | - 使用 `npm run lint` 来做代码风格检查。 35 | - 使用 `npm test` 来执行单元测试。 36 | - 使用 `npm run autod` 来自动检测依赖更新,详细参见 [autod](https://www.npmjs.com/package/autod) 。 37 | 38 | 39 | [egg]: https://eggjs.org 40 | -------------------------------------------------------------------------------- /app/middleware/error_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (option, app) => { 4 | return async function (ctx, next) { 5 | try { 6 | await next() 7 | } catch (err) { 8 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 9 | app.emit('error', err, this) 10 | const status = err.status || 500 11 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 12 | const error = status === 500 && app.config.env === 'prod' ? 13 | 'Internal Server Error' : 14 | err.message 15 | // 从 error 对象上读出各个属性,设置到响应中 16 | ctx.body = { 17 | code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 18 | error: error 19 | } 20 | if (status === 422) { 21 | ctx.body.detail = err.errors 22 | } 23 | ctx.status = 200 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /app/extend/RESTfulHTTPStatus.rec: -------------------------------------------------------------------------------- 1 | 200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。 2 | 201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。 3 | 202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务) 4 | 204 NO CONTENT - [DELETE]:用户删除数据成功。 5 | 400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作,该操作是幂等的。 6 | 401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。 7 | 403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。 8 | 404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。 9 | 406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。 10 | 410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。 11 | 422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。 12 | 500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功 13 | 14 | /** 15 | * 在接口处理发生错误的时候,如果是客户端请求参数导致的错误,我们会返回 4xx 状态码, 16 | * 如果是服务端自身的处理逻辑错误,我们会返回 5xx 状态码。 17 | * 所有的异常对象都是对这个异常状态的描述,其中 error 字段是错误的描述,detail 字段(可选)是导致错误的详细原因。 18 | */ 19 | 20 | eg----: 21 | 533 INTERNAL SERVER ERROR - [*]:keystore 不存在 22 | 534 INTERNAL SERVER ERROR - [*]:keystore 已过期 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # egg-RESTfulAPI 2 | 3 | 基于Egg.js的 RESTful API 模板,用于快速集成开发RESTful前后端分离的服务端。 4 | (建议用于学习入门EGGJS和Mongoose,如果作为生产请自行优化和改造) 5 | 6 | ## 特性 7 | 8 | - :zap: **框架选择**:基于 Egg.js 2.0 9 | - :fire: **数据模型**:基于 Mongoose 存储 10 | - :lock: **授权验证**:基于JWT 11 | - :rocket: **内置功能**:文件处理,用户系统,统一错误处理及接口返回标准,全方位CRUD,分页,模糊查询的等数据操作Demo 12 | - :sparkles: **最佳实践**:接口设计适配 Ant Design Pro 或 微信小程序开发等。(内置分页及ant接口返回标准) 13 | 14 | ## QuickStart 15 | 16 | 17 | 18 | see [egg docs][egg] for more detail. 19 | 20 | ### Development 21 | 22 | ```bash 23 | $ cd app & mkdir public & cd public & mkdir uploads 24 | $ npm i 25 | $ npm run dev 26 | $ open http://localhost:7001/ 27 | ``` 28 | 29 | ### Deploy 30 | 31 | ```bash 32 | $ npm start 33 | $ npm stop 34 | ``` 35 | 36 | ### npm scripts 37 | 38 | - Use `npm run lint` to check code style. 39 | - Use `npm test` to run unit test. 40 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. 41 | 42 | 43 | [egg]: https://eggjs.org 44 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | module.exports = appInfo => { 2 | const config = exports = {} 3 | 4 | // use for cookie sign key, should change to your own and keep security 5 | config.keys = appInfo.name + '_1513779989145_1674' 6 | 7 | // add your config here 8 | // 加载 errorHandler 中间件 9 | config.middleware = [ 'errorHandler' ] 10 | 11 | // 只对 /api 前缀的 url 路径生效 12 | // config.errorHandler = { 13 | // match: '/api', 14 | // } 15 | 16 | config.security = { 17 | csrf: { 18 | enable: false, 19 | }, 20 | domainWhiteList: [ 'http://localhost:8000' ], 21 | } 22 | 23 | config.multipart = { 24 | fileExtensions: [ '.apk', '.pptx', '.docx', '.csv', '.doc', '.ppt', '.pdf', '.pages', '.wav', '.mov' ], // 增加对 .apk 扩展名的支持 25 | }, 26 | 27 | config.bcrypt = { 28 | saltRounds: 10 // default 10 29 | } 30 | 31 | config.mongoose = { 32 | url: 'mongodb://127.0.0.1:27017/egg_x', 33 | options: { 34 | useMongoClient: true, 35 | autoReconnect: true, 36 | reconnectTries: Number.MAX_VALUE, 37 | bufferMaxEntries: 0, 38 | }, 39 | } 40 | 41 | config.jwt = { 42 | secret: 'Great4-M', 43 | enable: true, // default is false 44 | match: '/jwt', // optional 45 | } 46 | 47 | return config 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Egg Debug", 11 | "runtimeExecutable": "npm", 12 | "runtimeArgs": [ 13 | "run", 14 | "debug" 15 | ], 16 | "console": "integratedTerminal", 17 | "restart": true, 18 | "protocol": "auto", 19 | "port": 9999 20 | }, 21 | { 22 | "type": "node", 23 | "request": "launch", 24 | "name": "Egg Debug with brk", 25 | "runtimeExecutable": "npm", 26 | "runtimeArgs": [ 27 | "run", 28 | "debug", 29 | "--", 30 | "--inspect-brk" 31 | ], 32 | "protocol": "inspector", 33 | "port": 9229 34 | }, 35 | { 36 | "type": "node", 37 | "request": "launch", 38 | "name": "Egg Test", 39 | "runtimeExecutable": "npm", 40 | "runtimeArgs": [ 41 | "run", 42 | "test-local", 43 | "--", 44 | "--inspect-brk" 45 | ], 46 | "protocol": "auto", 47 | "port": 9229 48 | }, 49 | { 50 | "type": "node", 51 | "request": "attach", 52 | "name": "Egg Attach to remote", 53 | "localRoot": "${workspaceRoot}", 54 | "remoteRoot": "/usr/src/app", 55 | "address": "localhost", 56 | "protocol": "auto", 57 | "port": 9999 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egg-RESTfulAPI", 3 | "version": "1.0.0", 4 | "description": "A optimized Node.js RESTful API Server Template based on egg.js!", 5 | "private": true, 6 | "dependencies": { 7 | "await-stream-ready": "^1.0.1", 8 | "egg": "^2.0.0", 9 | "egg-bcrypt": "^1.0.0", 10 | "egg-cors": "^2.0.0", 11 | "egg-jwt": "^2.2.0", 12 | "egg-mongoose": "^2.1.1", 13 | "egg-scripts": "^2.1.0", 14 | "egg-validate": "^1.0.0", 15 | "image-downloader": "^3.3.0", 16 | "mocha": "^4.0.1", 17 | "moment": "^2.20.1", 18 | "stream-to-array": "^2.3.0", 19 | "stream-wormhole": "^1.0.3" 20 | }, 21 | "devDependencies": { 22 | "autod": "^3.0.1", 23 | "autod-egg": "^1.0.0", 24 | "egg-bin": "^4.3.5", 25 | "egg-ci": "^1.8.0", 26 | "egg-mock": "^3.13.0", 27 | "eslint": "^4.11.0", 28 | "eslint-config-egg": "^5.1.0", 29 | "webstorm-disable-index": "^1.2.0" 30 | }, 31 | "engines": { 32 | "node": ">=8.9.0" 33 | }, 34 | "scripts": { 35 | "start": "egg-scripts start --daemon", 36 | "stop": "egg-scripts stop", 37 | "dev": "egg-bin dev", 38 | "debug": "egg-bin debug", 39 | "test": "npm run lint -- --fix && npm run test-local", 40 | "test-local": "egg-bin test", 41 | "cov": "egg-bin cov", 42 | "lint": "eslint .", 43 | "ci": "npm run lint && npm run cov", 44 | "autod": "autod" 45 | }, 46 | "ci": { 47 | "version": "8" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "" 52 | }, 53 | "author": "lpx@4-m.cn", 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * @param {Egg.Application} app - egg application 4 | */ 5 | module.exports = app => { 6 | const { router, controller } = app 7 | router.get('/', controller.home.index) 8 | 9 | // role 10 | // router.post('/api/role', controller.role.create) 11 | // router.delete('/api/role/:id', controller.role.destroy) 12 | // router.put('/api/role/:id', controller.role.update) 13 | // router.get('/api/role/:id', controller.role.show) 14 | // router.get('/api/role', controller.role.index) 15 | router.delete('/api/role', controller.role.removes) 16 | router.resources('role', '/api/role', controller.role) 17 | 18 | // userAccess 19 | router.post('/api/user/access/login', controller.userAccess.login) 20 | router.get('/api/user/access/current', app.jwt, controller.userAccess.current) 21 | router.get('/api/user/access/logout', controller.userAccess.logout) 22 | router.put('/api/user/access/resetPsw', app.jwt, controller.userAccess.resetPsw) 23 | 24 | // user 25 | // router.post('/api/user', controller.user.create) 26 | // router.delete('/api/user/:id', controller.user.destroy) 27 | // router.put('/api/user/:id', controller.user.update) 28 | // router.get('/api/user/:id', controller.user.show) 29 | // router.get('/api/user', controller.user.index) 30 | router.delete('/api/user', controller.user.removes) 31 | router.resources('user', '/api/user', controller.user) 32 | 33 | // upload 34 | router.post('/api/upload', controller.upload.create) 35 | router.post('/api/upload/url', controller.upload.url) 36 | router.post('/api/uploads', controller.upload.multiple) 37 | router.delete('/api/upload/:id', controller.upload.destroy) 38 | // router.put('/api/upload/:id', controller.upload.update) 39 | router.post('/api/upload/:id', controller.upload.update) // Ant Design Pro 40 | router.put('/api/upload/:id/extra', controller.upload.extra) 41 | router.get('/api/upload/:id', controller.upload.show) 42 | router.get('/api/upload', controller.upload.index) 43 | router.delete('/api/upload', controller.upload.removes) 44 | // router.resources('upload', '/api/upload', controller.upload) 45 | } 46 | -------------------------------------------------------------------------------- /app/service/userAccess.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Service = require('egg').Service 4 | 5 | class UserAccessService extends Service { 6 | 7 | async login(payload) { 8 | const { ctx, service } = this 9 | const user = await service.user.findByMobile(payload.mobile) 10 | if(!user){ 11 | ctx.throw(404, 'user not found') 12 | } 13 | let verifyPsw = await ctx.compare(payload.password, user.password) 14 | if(!verifyPsw) { 15 | ctx.throw(404, 'user password is error') 16 | } 17 | // 生成Token令牌 18 | return { token: await service.actionToken.apply(user._id) } 19 | } 20 | 21 | async logout() { 22 | } 23 | 24 | async resetPsw(values) { 25 | const { ctx, service } = this 26 | // ctx.state.user 可以提取到JWT编码的data 27 | const _id = ctx.state.user.data._id 28 | const user = await service.user.find(_id) 29 | if (!user) { 30 | ctx.throw(404, 'user is not found') 31 | } 32 | 33 | let verifyPsw = await ctx.compare(values.oldPassword, user.password) 34 | if (!verifyPsw) { 35 | ctx.throw(404, 'user password error') 36 | } else { 37 | // 重置密码 38 | values.password = await ctx.genHash(values.password) 39 | return service.user.findByIdAndUpdate(_id, values) 40 | } 41 | } 42 | 43 | async current() { 44 | const { ctx, service } = this 45 | // ctx.state.user 可以提取到JWT编码的data 46 | const _id = ctx.state.user.data._id 47 | const user = await service.user.find(_id) 48 | if (!user) { 49 | ctx.throw(404, 'user is not found') 50 | } 51 | user.password = 'How old are you?' 52 | return user 53 | } 54 | 55 | // 修改个人信息 56 | async resetSelf(values) { 57 | const {ctx, service} = this 58 | // 获取当前用户 59 | const _id = ctx.state.user.data._id 60 | const user = await service.user.find(_id) 61 | if (!user) { 62 | ctx.throw(404, 'user is not found') 63 | } 64 | return service.user.findByIdAndUpdate(_id, values) 65 | } 66 | 67 | // 更新头像 68 | async resetAvatar(values) { 69 | const {ctx, service} = this 70 | await service.upload.create(values) 71 | // 获取当前用户 72 | const _id = ctx.state.user.data._id 73 | const user = await service.user.find(_id) 74 | if (!user) { 75 | ctx.throw(404, 'user is not found') 76 | } 77 | return service.user.findByIdAndUpdate(_id, {avatar: values.url}) 78 | } 79 | 80 | } 81 | 82 | module.exports = UserAccessService 83 | -------------------------------------------------------------------------------- /app/controller/role.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | 3 | class RoleController extends Controller { 4 | constructor(ctx) { 5 | super(ctx) 6 | 7 | this.createRule = { 8 | name: { type: 'string', required: true, allowEmpty: false }, 9 | access: { type: 'string', required: true, allowEmpty: false } 10 | } 11 | 12 | } 13 | 14 | // 创建角色 15 | async create() { 16 | const { ctx, service } = this 17 | // 校验参数 18 | ctx.validate(this.createRule) 19 | // 组装参数 20 | const payload = ctx.request.body || {} 21 | // 调用 Service 进行业务处理 22 | const res = await service.role.create(payload) 23 | // 设置响应内容和响应状态码 24 | ctx.helper.success({ctx, res}) 25 | } 26 | 27 | // 删除单个角色 28 | async destroy() { 29 | const { ctx, service } = this 30 | // 校验参数 31 | const { id } = ctx.params 32 | // 调用 Service 进行业务处理 33 | await service.role.destroy(id) 34 | // 设置响应内容和响应状态码 35 | ctx.helper.success({ctx}) 36 | } 37 | 38 | // 修改角色 39 | async update() { 40 | const { ctx, service } = this 41 | // 校验参数 42 | ctx.validate(this.createRule) 43 | // 组装参数 44 | const { id } = ctx.params 45 | const payload = ctx.request.body || {} 46 | // 调用 Service 进行业务处理 47 | await service.role.update(id, payload) 48 | // 设置响应内容和响应状态码 49 | ctx.helper.success({ctx}) 50 | } 51 | 52 | // 获取单个角色 53 | async show() { 54 | const { ctx, service } = this 55 | // 组装参数 56 | const { id } = ctx.params 57 | // 调用 Service 进行业务处理 58 | const res = await service.role.show(id) 59 | // 设置响应内容和响应状态码 60 | ctx.helper.success({ctx, res}) 61 | } 62 | 63 | // 获取所有角色(分页/模糊) 64 | async index() { 65 | const { ctx, service } = this 66 | // 组装参数 67 | const payload = ctx.query 68 | // 调用 Service 进行业务处理 69 | const res = await service.role.index(payload) 70 | // 设置响应内容和响应状态码 71 | ctx.helper.success({ctx, res}) 72 | } 73 | 74 | // 删除所选角色(条件id[]) 75 | async removes() { 76 | const { ctx, service } = this 77 | // 组装参数 78 | // const payload = ctx.queries.id 79 | const { id } = ctx.request.body // {id: "5a452a44ab122b16a0231b42,5a452a3bab122b16a0231b41"} 80 | const payload = id.split(',') || [] 81 | // 调用 Service 进行业务处理 82 | const result = await service.role.removes(payload) 83 | // 设置响应内容和响应状态码 84 | ctx.helper.success({ctx}) 85 | } 86 | 87 | } 88 | 89 | 90 | module.exports = RoleController -------------------------------------------------------------------------------- /app/controller/user.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | 3 | class UserController extends Controller { 4 | constructor(ctx) { 5 | super(ctx) 6 | 7 | this.UserCreateTransfer = { 8 | mobile: {type: 'string', required: true, allowEmpty: false, format: /^[0-9]{11}$/}, 9 | password: {type: 'password', required: true, allowEmpty: false, min: 6}, 10 | realName: {type: 'string', required: true, allowEmpty: false, format: /^[\u2E80-\u9FFF]{2,6}$/} 11 | } 12 | 13 | this.UserUpdateTransfer = { 14 | mobile: { type: 'string', required: true, allowEmpty: false }, 15 | realName: {type: 'string', required: true, allowEmpty: false, format: /^[\u2E80-\u9FFF]{2,6}$/} 16 | } 17 | } 18 | 19 | // 创建用户 20 | async create() { 21 | const { ctx, service } = this 22 | // 校验参数 23 | ctx.validate(this.UserCreateTransfer) 24 | // 组装参数 25 | const payload = ctx.request.body || {} 26 | // 调用 Service 进行业务处理 27 | const res = await service.user.create(payload) 28 | // 设置响应内容和响应状态码 29 | ctx.helper.success({ctx, res}) 30 | } 31 | 32 | // 删除单个用户 33 | async destroy() { 34 | const { ctx, service } = this 35 | // 校验参数 36 | const { id } = ctx.params 37 | // 调用 Service 进行业务处理 38 | await service.user.destroy(id) 39 | // 设置响应内容和响应状态码 40 | ctx.helper.success({ctx}) 41 | } 42 | 43 | // 修改用户 44 | async update() { 45 | const { ctx, service } = this 46 | // 校验参数 47 | ctx.validate(this.UserUpdateTransfer) 48 | // 组装参数 49 | const { id } = ctx.params 50 | const payload = ctx.request.body || {} 51 | // 调用 Service 进行业务处理 52 | await service.user.update(id, payload) 53 | // 设置响应内容和响应状态码 54 | ctx.helper.success({ctx}) 55 | } 56 | 57 | // 获取单个用户 58 | async show() { 59 | const { ctx, service } = this 60 | // 组装参数 61 | const { id } = ctx.params 62 | // 调用 Service 进行业务处理 63 | const res = await service.user.show(id) 64 | // 设置响应内容和响应状态码 65 | ctx.helper.success({ctx, res}) 66 | } 67 | 68 | // 获取所有用户(分页/模糊) 69 | async index() { 70 | const { ctx, service } = this 71 | // 组装参数 72 | const payload = ctx.query 73 | // 调用 Service 进行业务处理 74 | const res = await service.user.index(payload) 75 | // 设置响应内容和响应状态码 76 | ctx.helper.success({ctx, res}) 77 | } 78 | 79 | // 删除所选用户(条件id[]) 80 | async removes() { 81 | const { ctx, service } = this 82 | // 组装参数 83 | // const payload = ctx.queries.id 84 | const { id } = ctx.request.body 85 | const payload = id.split(',') || [] 86 | // 调用 Service 进行业务处理 87 | const result = await service.user.removes(payload) 88 | // 设置响应内容和响应状态码 89 | ctx.helper.success({ctx}) 90 | } 91 | 92 | } 93 | 94 | 95 | module.exports = UserController -------------------------------------------------------------------------------- /app/service/role.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service 2 | 3 | class RoleService extends Service { 4 | // create======================================================================================================> 5 | async create(payload) { 6 | return this.ctx.model.Role.create(payload) 7 | } 8 | 9 | // destroy======================================================================================================> 10 | async destroy(_id) { 11 | const { ctx, service } = this 12 | const role = await ctx.service.role.find(_id) 13 | if (!role) { 14 | ctx.throw(404, 'role not found') 15 | } 16 | return ctx.model.Role.findByIdAndRemove(_id) 17 | } 18 | 19 | // update======================================================================================================> 20 | async update(_id, payload) { 21 | const { ctx, service } = this 22 | const role = await ctx.service.role.find(_id) 23 | if (!role) { 24 | ctx.throw(404, 'role not found') 25 | } 26 | return ctx.model.Role.findByIdAndUpdate(_id, payload) 27 | } 28 | 29 | // show======================================================================================================> 30 | async show(_id) { 31 | const role = await this.ctx.service.role.find(_id) 32 | if (!role) { 33 | this.ctx.throw(404, 'role not found') 34 | } 35 | return this.ctx.model.Role.findById(_id) 36 | } 37 | 38 | // index======================================================================================================> 39 | async index(payload) { 40 | const { currentPage, pageSize, isPaging, search } = payload 41 | let res = [] 42 | let count = 0 43 | let skip = ((Number(currentPage)) - 1) * Number(pageSize || 10) 44 | if(isPaging) { 45 | if(search) { 46 | res = await this.ctx.model.Role.find({name: { $regex: search } }).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 47 | count = res.length 48 | } else { 49 | res = await this.ctx.model.Role.find({}).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 50 | count = await this.ctx.model.Role.count({}).exec() 51 | } 52 | } else { 53 | if(search) { 54 | res = await this.ctx.model.Role.find({name: { $regex: search } }).sort({ createdAt: -1 }).exec() 55 | count = res.length 56 | } else { 57 | res = await this.ctx.model.Role.find({}).sort({ createdAt: -1 }).exec() 58 | count = await this.ctx.model.Role.count({}).exec() 59 | } 60 | } 61 | // 整理数据源 -> Ant Design Pro 62 | let data = res.map((e,i) => { 63 | const jsonObject = Object.assign({}, e._doc) 64 | jsonObject.key = i 65 | jsonObject.createdAt = this.ctx.helper.formatTime(e.createdAt) 66 | return jsonObject 67 | }) 68 | 69 | return { count: count, list: data, pageSize: Number(pageSize), currentPage: Number(currentPage) } 70 | } 71 | 72 | // removes======================================================================================================> 73 | async removes(values) { 74 | return this.ctx.model.Role.remove({ _id: { $in: values } }) 75 | } 76 | 77 | // Commons======================================================================================================> 78 | async find(id) { 79 | return this.ctx.model.Role.findById(id) 80 | } 81 | 82 | } 83 | 84 | module.exports = RoleService -------------------------------------------------------------------------------- /app/controller/userAccess.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = require('fs') 3 | const path = require('path') 4 | const awaitWriteStream = require('await-stream-ready').write 5 | const sendToWormhole = require('stream-wormhole') 6 | const download = require('image-downloader') 7 | const Controller = require('egg').Controller 8 | 9 | class UserAccessController extends Controller { 10 | 11 | constructor(ctx) { 12 | super(ctx) 13 | 14 | this.UserLoginTransfer = { 15 | mobile: { type: 'string', required: true, allowEmpty: false }, 16 | password: { type: 'string', required: true, allowEmpty: false } 17 | } 18 | 19 | this.UserResetPswTransfer = { 20 | password: { type: 'password', required: true, allowEmpty: false, min: 6 }, 21 | oldPassword: { type: 'password', required: true, allowEmpty: false, min: 6 } 22 | } 23 | 24 | this.UserUpdateTransfer = { 25 | mobile: { type: 'string', required: true, allowEmpty: false }, 26 | realName: {type: 'string', required: true, allowEmpty: false, format: /^[\u2E80-\u9FFF]{2,6}$/} 27 | } 28 | } 29 | 30 | // 用户登入 31 | async login() { 32 | const { ctx, service } = this 33 | // 校验参数 34 | ctx.validate(this.UserLoginTransfer) 35 | // 组装参数 36 | const payload = ctx.request.body || {} 37 | // 调用 Service 进行业务处理 38 | const res = await service.userAccess.login(payload) 39 | // 设置响应内容和响应状态码 40 | ctx.helper.success({ctx, res}) 41 | } 42 | 43 | // 用户登出 44 | async logout() { 45 | const { ctx, service } = this 46 | // 调用 Service 进行业务处理 47 | await service.userAccess.logout() 48 | // 设置响应内容和响应状态码 49 | ctx.helper.success({ctx}) 50 | } 51 | 52 | // 修改密码 53 | async resetPsw() { 54 | const { ctx, service } = this 55 | // 校验参数 56 | ctx.validate(this.UserResetPswTransfer) 57 | // 组装参数 58 | const payload = ctx.request.body || {} 59 | // 调用 Service 进行业务处理 60 | await service.userAccess.resetPsw(payload) 61 | // 设置响应内容和响应状态码 62 | ctx.helper.success({ctx}) 63 | } 64 | 65 | // 获取用户信息 66 | async current() { 67 | const { ctx, service } = this 68 | const res = await service.userAccess.current() 69 | // 设置响应内容和响应状态码 70 | ctx.helper.success({ctx, res}) 71 | } 72 | 73 | // 修改基础信息 74 | async resetSelf() { 75 | const {ctx, service} = this 76 | // 校验参数 77 | ctx.validate(this.UserUpdateTransfer) 78 | // 组装参数 79 | const payload = ctx.request.body || {} 80 | // 调用Service 进行业务处理 81 | await service.userAccess.resetSelf(payload) 82 | // 设置响应内容和响应状态码 83 | ctx.helper.success({ctx}) 84 | } 85 | 86 | // 修改头像 87 | async resetAvatar() { 88 | const { ctx, service } = this 89 | const stream = await ctx.getFileStream() 90 | const filename = path.basename(stream.filename) 91 | const extname = path.extname(stream.filename).toLowerCase() 92 | const attachment = new this.ctx.model.Attachment 93 | attachment.extname = extname 94 | attachment.filename = filename 95 | attachment.url = `/uploads/avatar/${attachment._id.toString()}${extname}` 96 | const target = path.join(this.config.baseDir, 'app/public/uploads/avatar', `${attachment._id.toString()}${attachment.extname}`) 97 | const writeStream = fs.createWriteStream(target) 98 | try { 99 | await awaitWriteStream(stream.pipe(writeStream)) 100 | // 调用 Service 进行业务处理 101 | await service.userAccess.resetAvatar(attachment) 102 | } catch (err) { 103 | await sendToWormhole(stream) 104 | throw err 105 | } 106 | // 设置响应内容和响应状态码 107 | ctx.helper.success({ctx}) 108 | } 109 | } 110 | 111 | module.exports = UserAccessController 112 | -------------------------------------------------------------------------------- /app/service/user.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service 2 | 3 | class UserService extends Service { 4 | // create======================================================================================================> 5 | async create(payload) { 6 | const { ctx, service } = this 7 | const role = await service.role.show(payload.role) 8 | if (!role) { 9 | ctx.throw(404, 'role is not found') 10 | } 11 | payload.password = await this.ctx.genHash(payload.password) 12 | return ctx.model.User.create(payload) 13 | } 14 | 15 | // destroy======================================================================================================> 16 | async destroy(_id) { 17 | const { ctx, service } = this 18 | const user = await ctx.service.user.find(_id) 19 | if (!user) { 20 | ctx.throw(404, 'user not found') 21 | } 22 | return ctx.model.User.findByIdAndRemove(_id) 23 | } 24 | 25 | // update======================================================================================================> 26 | async update(_id, payload) { 27 | const { ctx, service } = this 28 | const user = await ctx.service.user.find(_id) 29 | if (!user) { 30 | ctx.throw(404, 'user not found') 31 | } 32 | return ctx.model.User.findByIdAndUpdate(_id, payload) 33 | } 34 | 35 | // show======================================================================================================> 36 | async show(_id) { 37 | const user = await this.ctx.service.user.find(_id) 38 | if (!user) { 39 | this.ctx.throw(404, 'user not found') 40 | } 41 | return this.ctx.model.User.findById(_id).populate('role') 42 | } 43 | 44 | // index======================================================================================================> 45 | async index(payload) { 46 | const { currentPage, pageSize, isPaging, search } = payload 47 | let res = [] 48 | let count = 0 49 | let skip = ((Number(currentPage)) - 1) * Number(pageSize || 10) 50 | if(isPaging) { 51 | if(search) { 52 | res = await this.ctx.model.User.find({mobile: { $regex: search } }).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 53 | count = res.length 54 | } else { 55 | res = await this.ctx.model.User.find({}).populate('role').skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 56 | count = await this.ctx.model.User.count({}).exec() 57 | } 58 | } else { 59 | if(search) { 60 | res = await this.ctx.model.User.find({mobile: { $regex: search } }).populate('role').sort({ createdAt: -1 }).exec() 61 | count = res.length 62 | } else { 63 | res = await this.ctx.model.User.find({}).populate('role').sort({ createdAt: -1 }).exec() 64 | count = await this.ctx.model.User.count({}).exec() 65 | } 66 | } 67 | // 整理数据源 -> Ant Design Pro 68 | let data = res.map((e,i) => { 69 | const jsonObject = Object.assign({}, e._doc) 70 | jsonObject.key = i 71 | jsonObject.password = 'Are you ok?' 72 | jsonObject.createdAt = this.ctx.helper.formatTime(e.createdAt) 73 | return jsonObject 74 | }) 75 | 76 | return { count: count, list: data, pageSize: Number(pageSize), currentPage: Number(currentPage) } 77 | } 78 | 79 | 80 | async removes(payload) { 81 | return this.ctx.model.User.remove({ _id: { $in: payload } }) 82 | } 83 | 84 | // Commons======================================================================================================> 85 | async findByMobile(mobile) { 86 | return this.ctx.model.User.findOne({mobile: mobile}) 87 | } 88 | 89 | async find(id) { 90 | return this.ctx.model.User.findById(id) 91 | } 92 | 93 | async findByIdAndUpdate(id, values) { 94 | return this.ctx.model.User.findByIdAndUpdate(id, values) 95 | } 96 | 97 | 98 | 99 | } 100 | 101 | 102 | module.exports = UserService -------------------------------------------------------------------------------- /app/service/upload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const awaitWriteStream = require('await-stream-ready').write 4 | const sendToWormhole = require('stream-wormhole') 5 | const Service = require('egg').Service 6 | 7 | class UploadService extends Service { 8 | 9 | async create(payload) { 10 | return this.ctx.model.Attachment.create(payload) 11 | } 12 | 13 | // destroy======================================================================================================> 14 | async destroy(_id) { 15 | const { ctx, service } = this 16 | const attachment = await ctx.service.upload.find(_id) 17 | if (!attachment) { 18 | ctx.throw(404, 'attachment not found') 19 | }else{ 20 | const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id}${attachment.extname}`) 21 | fs.unlinkSync(target) 22 | } 23 | return ctx.model.Attachment.findByIdAndRemove(_id) 24 | } 25 | 26 | // update======================================================================================================> 27 | async updatePre(_id) { 28 | const { ctx, service } = this 29 | const attachment = await ctx.service.upload.find(_id) 30 | if (!attachment) { 31 | ctx.throw(404, 'attachment not found') 32 | }else{ 33 | const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id}${attachment.extname}`) 34 | fs.unlinkSync(target) 35 | } 36 | return attachment 37 | } 38 | 39 | async extra(_id, values) { 40 | const { ctx, service } = this 41 | const attachment = await ctx.service.upload.find(_id) 42 | if (!attachment) { 43 | ctx.throw(404, 'attachment not found') 44 | } 45 | return this.ctx.model.Attachment.findByIdAndUpdate(_id, values) 46 | } 47 | 48 | async update(_id, values) { 49 | return this.ctx.model.Attachment.findByIdAndUpdate(_id, values) 50 | } 51 | 52 | // show======================================================================================================> 53 | async show(_id) { 54 | const attachment = await this.ctx.service.upload.find(_id) 55 | if (!attachment) { 56 | this.ctx.throw(404, 'attachment not found') 57 | } 58 | return this.ctx.model.Attachment.findById(_id) 59 | } 60 | 61 | // index======================================================================================================> 62 | async index(payload) { 63 | // 支持全部all 无需传入kind 64 | // 图像kind = image ['.jpg', '.jpeg', '.png', '.gif'] 65 | // 文档kind = document ['.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.csv', '.key', '.numbers', '.pages', '.pdf', '.txt', '.psd', '.zip', '.gz', '.tgz', '.gzip' ] 66 | // 视频kind = video ['.mov', '.mp4', '.avi'] 67 | // 音频kind = audio ['.mp3', '.wma', '.wav', '.ogg', '.ape', '.acc'] 68 | 69 | const attachmentKind = { 70 | image: ['.jpg', '.jpeg', '.png', '.gif'], 71 | document: ['.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.csv', '.key', '.numbers', '.pages', '.pdf', '.txt', '.psd', '.zip', '.gz', '.tgz', '.gzip' ], 72 | video: ['.mov', '.mp4', '.avi'], 73 | audio: ['.mp3', '.wma', '.wav', '.ogg', '.ape', '.acc'] 74 | } 75 | 76 | let { currentPage, pageSize, isPaging, search, kind } = payload 77 | let res = [] 78 | let count = 0 79 | let skip = ((Number(currentPage)) - 1) * Number(pageSize || 10) 80 | if(isPaging) { 81 | if(search) { 82 | if (kind) { 83 | res = await this.ctx.model.Attachment.find({filename: { $regex: search }, extname: { $in: attachmentKind[`${kind}`]} }).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 84 | }else{ 85 | res = await this.ctx.model.Attachment.find({filename: { $regex: search } }).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 86 | } 87 | count = res.length 88 | } else { 89 | if (kind) { 90 | res = await this.ctx.model.Attachment.find({ extname: { $in: attachmentKind[`${kind}`]} }).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 91 | count = await this.ctx.model.Attachment.count({ extname: { $in: attachmentKind[`${kind}`]} }).exec() 92 | }else{ 93 | res = await this.ctx.model.Attachment.find({}).skip(skip).limit(Number(pageSize)).sort({ createdAt: -1 }).exec() 94 | count = await this.ctx.model.Attachment.count({}).exec() 95 | } 96 | } 97 | } else { 98 | if(search) { 99 | if (kind) { 100 | res = await this.ctx.model.Attachment.find({filename: { $regex: search }, extname: { $in: attachmentKind[`${kind}`]} }).sort({ createdAt: -1 }).exec() 101 | }else{ 102 | res = await this.ctx.model.Attachment.find({filename: { $regex: search }}).sort({ createdAt: -1 }).exec() 103 | } 104 | count = res.length 105 | } else { 106 | if (kind) { 107 | res = await this.ctx.model.Attachment.find({extname: { $in: attachmentKind[`${kind}`]} }).sort({ createdAt: -1 }).exec() 108 | count = await this.ctx.model.Attachment.count({ extname: { $in: attachmentKind[`${kind}`]} }).exec() 109 | }else{ 110 | res = await this.ctx.model.Attachment.find({}).sort({ createdAt: -1 }).exec() 111 | count = await this.ctx.model.Attachment.count({}).exec() 112 | } 113 | } 114 | } 115 | // 整理数据源 -> Ant Design Pro 116 | let data = res.map((e,i) => { 117 | const jsonObject = Object.assign({}, e._doc) 118 | jsonObject.key = i 119 | jsonObject.createdAt = this.ctx.helper.formatTime(e.createdAt) 120 | return jsonObject 121 | }) 122 | 123 | return { count: count, list: data, pageSize: Number(pageSize), currentPage: Number(currentPage) } 124 | } 125 | 126 | // Commons======================================================================================================> 127 | async find(id) { 128 | return this.ctx.model.Attachment.findById(id) 129 | } 130 | } 131 | 132 | module.exports = UploadService -------------------------------------------------------------------------------- /app/controller/upload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const Controller = require('egg').Controller 4 | const awaitWriteStream = require('await-stream-ready').write 5 | const sendToWormhole = require('stream-wormhole') 6 | const download = require('image-downloader') 7 | 8 | class UploadController extends Controller { 9 | constructor (ctx){ 10 | super(ctx) 11 | } 12 | 13 | // 上传单个文件 14 | async create() { 15 | const { ctx, service } = this 16 | // 要通过 ctx.getFileStream 便捷的获取到用户上传的文件,需要满足两个条件: 17 | // 只支持上传一个文件。 18 | // 上传文件必须在所有其他的 fields 后面,否则在拿到文件流时可能还获取不到 fields。 19 | const stream = await ctx.getFileStream() 20 | // 所有表单字段都能通过 `stream.fields` 获取到 21 | const filename = path.basename(stream.filename) // 文件名称 22 | const extname = path.extname(stream.filename).toLowerCase() // 文件扩展名称 23 | // 组装参数 model 24 | const attachment = new this.ctx.model.Attachment 25 | attachment.extname = extname 26 | attachment.filename = filename 27 | attachment.url = `/uploads/${attachment._id.toString()}${extname}` 28 | // 组装参数 stream 29 | const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${attachment.extname}`) 30 | const writeStream = fs.createWriteStream(target) 31 | // 文件处理,上传到云存储等等 32 | try { 33 | await awaitWriteStream(stream.pipe(writeStream)) 34 | } catch (err) { 35 | // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 36 | await sendToWormhole(stream) 37 | throw err 38 | } 39 | // 调用 Service 进行业务处理 40 | const res = await service.upload.create(attachment) 41 | // 设置响应内容和响应状态码 42 | ctx.helper.success({ctx, res}) 43 | } 44 | 45 | // 通过URL添加单个图片: 如果网络地址不合法,EGG会返回500错误 46 | async url() { 47 | const { ctx, service } = this 48 | // 组装参数 49 | const attachment = new this.ctx.model.Attachment 50 | const { url } = ctx.request.body 51 | const filename = path.basename(url) // 文件名称 52 | const extname = path.extname(url).toLowerCase() // 文件扩展名称 53 | const options = { 54 | url: url, 55 | dest: path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${extname}`) 56 | } 57 | let res 58 | try { 59 | // 写入文件 const { filename, image} 60 | await download.image(options) 61 | attachment.extname = extname 62 | attachment.filename = filename 63 | attachment.url = `/uploads/${attachment._id.toString()}${extname}` 64 | res = await service.upload.create(attachment) 65 | } catch (err) { 66 | throw err 67 | } 68 | // 设置响应内容和响应状态码 69 | ctx.helper.success({ctx, res}) 70 | } 71 | 72 | // 上传多个文件 73 | async multiple() { 74 | // 要获取同时上传的多个文件,不能通过 ctx.getFileStream() 来获取 75 | const { ctx, service } = this 76 | const parts = ctx.multipart() 77 | const res = {} 78 | const files = [] 79 | 80 | let part // parts() return a promise 81 | while ((part = await parts()) != null) { 82 | if (part.length) { 83 | // 如果是数组的话是 filed 84 | // console.log('field: ' + part[0]) 85 | // console.log('value: ' + part[1]) 86 | // console.log('valueTruncated: ' + part[2]) 87 | // console.log('fieldnameTruncated: ' + part[3]) 88 | } else { 89 | if (!part.filename) { 90 | // 这时是用户没有选择文件就点击了上传(part 是 file stream,但是 part.filename 为空) 91 | // 需要做出处理,例如给出错误提示消息 92 | return 93 | } 94 | // part 是上传的文件流 95 | // console.log('field: ' + part.fieldname) 96 | // console.log('filename: ' + part.filename) 97 | // console.log('extname: ' + part.extname) 98 | // console.log('encoding: ' + part.encoding) 99 | // console.log('mime: ' + part.mime) 100 | const filename = part.filename.toLowerCase() // 文件名称 101 | const extname = path.extname(part.filename).toLowerCase() // 文件扩展名称 102 | 103 | // 组装参数 104 | const attachment = new ctx.model.Attachment 105 | attachment.extname = extname 106 | attachment.filename = filename 107 | attachment.url = `/uploads/${attachment._id.toString()}${extname}` 108 | // const target = path.join(this.config.baseDir, 'app/public/uploads', filename) 109 | const target = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id.toString()}${extname}`) 110 | const writeStream = fs.createWriteStream(target) 111 | // 文件处理,上传到云存储等等 112 | let res 113 | try { 114 | // result = await ctx.oss.put('egg-multipart-test/' + part.filename, part) 115 | await awaitWriteStream(part.pipe(writeStream)) 116 | // 调用Service 117 | res = await service.upload.create(attachment) 118 | } catch (err) { 119 | // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 120 | await sendToWormhole(part) 121 | throw err 122 | } 123 | files.push(`${attachment._id}`) // console.log(result) 124 | } 125 | } 126 | ctx.helper.success({ctx, res: { _ids:files }}) 127 | } 128 | 129 | // 删除单个文件 130 | async destroy() { 131 | const { ctx, service } = this 132 | // 校验参数 133 | const { id } = ctx.params 134 | // 调用 Service 进行业务处理 135 | await service.upload.destroy(id) 136 | // 设置响应内容和响应状态码 137 | ctx.helper.success({ctx}) 138 | } 139 | 140 | // 修改单个文件 141 | async update() { 142 | const { ctx, service } = this 143 | // 组装参数 144 | const { id } = ctx.params // 传入要修改的文档ID 145 | // 调用Service 删除旧文件,如果存在 146 | const attachment = await service.upload.updatePre(id) 147 | // 获取用户上传的替换文件 148 | const stream = await ctx.getFileStream() 149 | const extname = path.extname(stream.filename).toLowerCase() // 文件扩展名称 150 | const filename = path.basename(stream.filename) // 文件名称 151 | // 组装更新参数 152 | attachment.extname = extname 153 | attachment.filename = filename 154 | attachment.url = `/uploads/${attachment._id.toString()}${extname}` 155 | const target_U = path.join(this.config.baseDir, 'app/public/uploads', `${attachment._id}${extname}`) 156 | const writeStream = fs.createWriteStream(target_U) 157 | // 文件处理,上传到云存储等等 158 | try { 159 | await awaitWriteStream(stream.pipe(writeStream)) 160 | } catch (err) { 161 | // 必须将上传的文件流消费掉,要不然浏览器响应会卡死 162 | await sendToWormhole(stream) 163 | throw err 164 | } 165 | // 调用Service 保持原图片ID不变,更新其他属性 166 | await service.upload.update(id, attachment) 167 | // 设置响应内容和响应状态码 168 | ctx.helper.success({ctx}) 169 | } 170 | 171 | // 添加图片描述 172 | async extra() { 173 | const { ctx, service } = this 174 | // 组装参数 175 | const { id } = ctx.params // 传入要修改的文档ID 176 | const payload = ctx.request.body || {} 177 | await service.upload.extra(id, payload) 178 | // 设置响应内容和响应状态码 179 | ctx.helper.success({ctx}) 180 | } 181 | 182 | // 获取单个文件 183 | async show() { 184 | const { ctx, service } = this 185 | // 组装参数 186 | const { id } = ctx.params 187 | // 调用 Service 进行业务处理 188 | const res = await service.upload.show(id) 189 | // 设置响应内容和响应状态码 190 | ctx.helper.success({ctx, res}) 191 | } 192 | 193 | // 获取所有文件(分页/模糊) 194 | async index() { 195 | const { ctx, service } = this 196 | // 组装参数 197 | const payload = ctx.query 198 | // 调用 Service 进行业务处理 199 | const res = await service.upload.index(payload) 200 | // 设置响应内容和响应状态码 201 | ctx.helper.success({ctx, res}) 202 | } 203 | 204 | // 删除所选文件(条件id[]) 205 | async removes() { 206 | const { ctx, service } = this 207 | // 组装参数 208 | // const values = ctx.queries.id 209 | const { id } = ctx.request.body 210 | const payload = id.split(',') || [] 211 | // 设置响应内容和响应状态码 212 | for (let attachment of payload) { 213 | // 调用 Service 进行业务处理 214 | await service.upload.destroy(attachment) 215 | } 216 | // 设置响应内容和响应状态码 217 | ctx.helper.success({ctx}) 218 | } 219 | } 220 | 221 | 222 | module.exports = UploadController -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | 2 | 3 | // 控制已更新文件的自动保存。接受的值:“off”、"afterDelay”、“onFocusChange”(编辑器失去焦点)、“onWindowChange”(窗口失去焦点)。如果设置为“afterDelay”,则可在 "files.autoSaveDelay" 中配置延迟。 4 | "files.autoSave": "off", 5 | 6 | // 以像素为单位控制字号。 7 | "editor.fontSize": 12, 8 | 9 | // 控制字体系列。 10 | "editor.fontFamily": "Menlo, Monaco, 'Courier New', monospace", 11 | 12 | // 一个制表符等于的空格数。该设置在 `editor.detectIndentation` 启用时根据文件内容进行重写。 13 | "editor.tabSize": 2, 14 | 15 | // 控制编辑器中呈现空白字符的方式,可能为“无”、“边界”和“全部”。“边界”选项不会在单词之间呈现单空格。 16 | "editor.renderWhitespace": "none", 17 | 18 | // 控制光标样式,接受的值为 "block"、"block-outline"、"line"、"line-thin" 、"underline" 和 "underline-thin" 19 | "editor.cursorStyle": "line", 20 | 21 | // 用鼠标添加多个光标时使用的修改键。“ctrlCmd”映射为“Control”(Windows 和 Linux)或“Command”(OSX)。“转到定义”和“打开链接”功能的鼠标手势将会相应调整,不与多光标修改键冲突。 22 | "editor.multiCursorModifier": "alt", 23 | 24 | // 按 "Tab" 时插入空格。该设置在 `editor.detectIndentation` 启用时根据文件内容进行重写。 25 | "editor.insertSpaces": true, 26 | 27 | // 控制折行方式。可以选择: - “off” (禁用折行), - “on” (视区折行), - “wordWrapColumn”(在“editor.wordWrapColumn”处折行)或 - “bounded”(在视区与“editor.wordWrapColumn”两者的较小者处折行)。 28 | "editor.wordWrap": "off", 29 | 30 | // 配置 glob 模式以在搜索中排除文件和文件夹。例如,文件资源管理器根据此设置决定文件或文件夹的显示和隐藏。 31 | "files.exclude": { 32 | "**/.git": true, 33 | "**/.svn": true, 34 | "**/.hg": true, 35 | "**/CVS": true, 36 | "**/.DS_Store": true 37 | }, 38 | 39 | // 配置语言的文件关联(如: "*.extension": "html")。这些关联的优先级高于已安装语言的默认关联。 40 | "files.associations": {} 41 | 42 | } 43 | , 44 | { 45 | 46 | 47 | // 当其前缀匹配时插入代码段。当 "quickSuggestions" 未启用时,效果最佳。 48 | "editor.tabCompletion": false, 49 | 50 | // 控制字体系列。 51 | "editor.fontFamily": "Menlo, Monaco, 'Courier New', monospace", 52 | 53 | // 控制字体粗细。 54 | "editor.fontWeight": "normal", 55 | 56 | // 以像素为单位控制字号。 57 | "editor.fontSize": 12, 58 | 59 | // 控制行高。使用 0 通过字号计算行高。 60 | "editor.lineHeight": 0, 61 | 62 | // 以像素为单位控制字符间距。 63 | "editor.letterSpacing": 0, 64 | 65 | // 控制行号的显示。可能的值为“开”、“关”和“相对”。“相对”将显示从当前光标位置开始计数的行数。 66 | "editor.lineNumbers": "on", 67 | 68 | // 在一定数量的等宽字符后显示垂直标尺。输入多个值,显示多个标尺。若数组为空,则不绘制标尺。 69 | "editor.rulers": [], 70 | 71 | // 执行文字相关的导航或操作时将用作文字分隔符的字符 72 | "editor.wordSeparators": "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?", 73 | 74 | // 一个制表符等于的空格数。该设置在 `editor.detectIndentation` 启用时根据文件内容进行重写。 75 | "editor.tabSize": 4, 76 | 77 | // 按 "Tab" 时插入空格。该设置在 `editor.detectIndentation` 启用时根据文件内容进行重写。 78 | "editor.insertSpaces": true, 79 | 80 | // 当打开文件时,将基于文件内容检测 "editor.tabSize" 和 "editor.insertSpaces"。 81 | "editor.detectIndentation": true, 82 | 83 | // 控制选取范围是否有圆角 84 | "editor.roundedSelection": true, 85 | 86 | // 控制编辑器是否可以滚动到最后一行之后 87 | "editor.scrollBeyondLastLine": true, 88 | 89 | // 控制编辑器是否在滚动时使用动画 90 | "editor.smoothScrolling": false, 91 | 92 | // 控制是否显示 minimap 93 | "editor.minimap.enabled": true, 94 | 95 | // 控制是否自动隐藏小地图滑块。可选值为 "always" 和 "mouseover" 96 | "editor.minimap.showSlider": "mouseover", 97 | 98 | // 呈现某行上的实际字符(与颜色块相反) 99 | "editor.minimap.renderCharacters": true, 100 | 101 | // 限制最小映射的宽度,尽量多地呈现特定数量的列 102 | "editor.minimap.maxColumn": 120, 103 | 104 | // 控制是否将编辑器的选中内容作为搜索词填入到查找组件 105 | "editor.find.seedSearchStringFromSelection": true, 106 | 107 | // 控制当编辑器中选中多个字符或多行文字时是否开启“在选定内容中查找”选项 108 | "editor.find.autoFindInSelection": false, 109 | 110 | // 控制折行方式。可以选择: - “off” (禁用折行), - “on” (视区折行), - “wordWrapColumn”(在“editor.wordWrapColumn”处折行)或 - “bounded”(在视区与“editor.wordWrapColumn”两者的较小者处折行)。 111 | "editor.wordWrap": "off", 112 | 113 | // 在 "editor.wordWrap" 为 "wordWrapColumn" 或 "bounded" 时控制编辑器列的换行。 114 | "editor.wordWrapColumn": 80, 115 | 116 | // 控制折行的缩进。可以是“none”、“same”或“indent”。 117 | "editor.wrappingIndent": "same", 118 | 119 | // 要对鼠标滚轮滚动事件的 "deltaX" 和 "deltaY" 使用的乘数 120 | "editor.mouseWheelScrollSensitivity": 1, 121 | 122 | // 用鼠标添加多个光标时使用的修改键。“ctrlCmd”映射为“Control”(Windows 和 Linux)或“Command”(OSX)。“转到定义”和“打开链接”功能的鼠标手势将会相应调整,不与多光标修改键冲突。 123 | "editor.multiCursorModifier": "alt", 124 | 125 | // 控制键入时是否应自动显示建议 126 | "editor.quickSuggestions": { 127 | "other": true, 128 | "comments": false, 129 | "strings": false 130 | }, 131 | 132 | // 控制延迟多少毫秒后将显示快速建议 133 | "editor.quickSuggestionsDelay": 10, 134 | 135 | // 启用在输入时显示含有参数文档和类型信息的小面板 136 | "editor.parameterHints": true, 137 | 138 | // 控制编辑器是否应该在左括号后自动插入右括号 139 | "editor.autoClosingBrackets": true, 140 | 141 | // 控制编辑器是否应在键入后自动设置行的格式 142 | "editor.formatOnType": false, 143 | 144 | // 控制编辑器是否应自动设置粘贴内容的格式。格式化程序必须可用并且能设置文档中某一范围的格式。 145 | "editor.formatOnPaste": false, 146 | 147 | // 控制编辑器是否在用户键入、粘贴或移动行时自动调整缩进。语言的缩进规则必须可用。 148 | "editor.autoIndent": true, 149 | 150 | // 控制键入触发器字符时是否应自动显示建议 151 | "editor.suggestOnTriggerCharacters": true, 152 | 153 | // 控制按“Enter”键是否像按“Tab”键一样接受建议。这能帮助避免“插入新行”和“接受建议”之间的歧义。值为“smart”时表示,仅当文字改变时,按“Enter”键才能接受建议 154 | "editor.acceptSuggestionOnEnter": "on", 155 | 156 | // 控制是否应在遇到提交字符时接受建议。例如,在 JavaScript 中,分号(";")可以为提交字符,可接受建议并键入该字符。 157 | "editor.acceptSuggestionOnCommitCharacter": true, 158 | 159 | // 控制是否将代码段与其他建议一起显示以及它们的排序方式。 160 | "editor.snippetSuggestions": "inline", 161 | 162 | // 控制没有选择内容的复制是否复制当前行。 163 | "editor.emptySelectionClipboard": true, 164 | 165 | // 控制是否应根据文档中的字数计算完成。 166 | "editor.wordBasedSuggestions": true, 167 | 168 | // 建议小组件的字号 169 | "editor.suggestFontSize": 0, 170 | 171 | // 建议小组件的行高 172 | "editor.suggestLineHeight": 0, 173 | 174 | // 控制编辑器是否应突出显示选项的近似匹配 175 | "editor.selectionHighlight": true, 176 | 177 | // 控制编辑器是否应该突出显示语义符号次数 178 | "editor.occurrencesHighlight": true, 179 | 180 | // 控制可在概述标尺同一位置显示的效果数量 181 | "editor.overviewRulerLanes": 3, 182 | 183 | // 控制概述标尺周围是否要绘制边框。 184 | "editor.overviewRulerBorder": true, 185 | 186 | // 控制光标动画样式,可能的值为 "blink"、"smooth"、"phase"、"expand" 和 "solid" 187 | "editor.cursorBlinking": "blink", 188 | 189 | // 通过使用鼠标滚轮同时按住 Ctrl 可缩放编辑器的字体 190 | "editor.mouseWheelZoom": false, 191 | 192 | // 控制光标样式,接受的值为 "block"、"block-outline"、"line"、"line-thin" 、"underline" 和 "underline-thin" 193 | "editor.cursorStyle": "line", 194 | 195 | // 启用字体连字 196 | "editor.fontLigatures": false, 197 | 198 | // 控制光标是否应隐藏在概述标尺中。 199 | "editor.hideCursorInOverviewRuler": false, 200 | 201 | // 控制编辑器中呈现空白字符的方式,可能为“无”、“边界”和“全部”。“边界”选项不会在单词之间呈现单空格。 202 | "editor.renderWhitespace": "none", 203 | 204 | // 控制编辑器是否应呈现控制字符 205 | "editor.renderControlCharacters": false, 206 | 207 | // 控制编辑器是否应呈现缩进参考线 208 | "editor.renderIndentGuides": true, 209 | 210 | // 控制编辑器应如何呈现当前行突出显示,可能为“无”、“装订线”、“线”和“全部”。 211 | "editor.renderLineHighlight": "line", 212 | 213 | // 控制编辑器是否显示代码滤镜 214 | "editor.codeLens": true, 215 | 216 | // 控制编辑器是否启用代码折叠功能 217 | "editor.folding": true, 218 | 219 | // 控制是否自动隐藏导航线上的折叠控件。 220 | "editor.showFoldingControls": "mouseover", 221 | 222 | // 当选择其中一项时,将突出显示匹配的括号。 223 | "editor.matchBrackets": true, 224 | 225 | // 控制编辑器是否应呈现垂直字形边距。字形边距最常用于调试。 226 | "editor.glyphMargin": true, 227 | 228 | // 在制表位后插入和删除空格 229 | "editor.useTabStops": true, 230 | 231 | // 删除尾随自动插入的空格 232 | "editor.trimAutoWhitespace": true, 233 | 234 | // 即使在双击编辑器内容或按 Esc 键时,也要保持速览编辑器的打开状态。 235 | "editor.stablePeek": false, 236 | 237 | // 控制编辑器是否应该允许通过拖放移动所选项。 238 | "editor.dragAndDrop": true, 239 | 240 | // 控制编辑器是否应运行在对屏幕阅读器进行优化的模式。 241 | "editor.accessibilitySupport": "auto", 242 | 243 | // 控制编辑器是否应检测链接并使它们可被点击 244 | "editor.links": true, 245 | 246 | // 控制编辑器是否显示内联颜色修饰器和颜色选取器。 247 | "editor.colorDecorators": true, 248 | 249 | // 启用代码操作小灯泡提示 250 | "editor.lightbulb.enabled": true, 251 | 252 | // 控制 Diff 编辑器以并排或内联形式显示差异 253 | "diffEditor.renderSideBySide": true, 254 | 255 | // 控制差异编辑器是否将对前导空格或尾随空格的更改显示为差异 256 | "diffEditor.ignoreTrimWhitespace": true, 257 | 258 | // 控制差异编辑器是否为已添加/删除的更改显示 +/- 指示符号 259 | "diffEditor.renderIndicators": true, 260 | 261 | // 保存时设置文件的格式。格式化程序必须可用,不能自动保存文件,并且不能关闭编辑器。 262 | "editor.formatOnSave": false, 263 | 264 | // 覆盖当前所选颜色主题中的编辑器颜色和字体样式。 265 | "editor.tokenColorCustomizations": {}, 266 | 267 | 268 | // 在未能恢复上一会话信息的情况下,控制启动时显示的编辑器。选择 "none" 表示启动时不打开编辑器,"welcomePage" 表示打开欢迎页面(默认),"newUntitledFile" 表示打开新的无标题文档(仅打开一个空工作区)。 269 | "workbench.startupEditor": "welcomePage", 270 | 271 | // 控制打开的编辑器是否显示在选项卡中。 272 | "workbench.editor.showTabs": true, 273 | 274 | // 控制编辑器标签的格式。修改这项设置会让文件的路径更容易理解: 275 | // - short: "parent" 276 | // - medium: "workspace/src/parent" 277 | // - long: "/home/user/workspace/src/parent" 278 | // - default: 当与另一选项卡标题相同时为 ".../parent"。选项卡被禁用时则为相对工作区路径 279 | "workbench.editor.labelFormat": "default", 280 | 281 | // 控制编辑器的选项卡关闭按钮的位置,或当设置为 "off" 时禁用关闭它们。 282 | "workbench.editor.tabCloseButton": "right", 283 | 284 | // 控制打开的编辑器是否随图标一起显示。这还需启用图标主题。 285 | "workbench.editor.showIcons": true, 286 | 287 | // 控制是否将打开的编辑器显示为预览。预览编辑器将会重用至其被保留(例如,通过双击或编辑),且其字体样式将为斜体。 288 | "workbench.editor.enablePreview": true, 289 | 290 | // 控制 Quick Open 中打开的编辑器是否显示为预览。预览编辑器可以重新使用,直到将其保留(例如,通过双击或编辑)。 291 | "workbench.editor.enablePreviewFromQuickOpen": true, 292 | 293 | // 控制打开编辑器的位置。选择“左侧”或“右侧”以在当前活动位置的左侧或右侧打开编辑器。选择“第一个”或“最后一个”以从当前活动位置独立打开编辑器。 294 | "workbench.editor.openPositioning": "right", 295 | 296 | // 控制打开时编辑器是否显示在任何可见组中。如果禁用,编辑器会优先在当前活动编辑器组中打开。如果启用,会显示已打开的编辑器而不是在当前活动编辑器组中再次打开。请注意,有些情况下会忽略此设置,例如强制编辑器在特定组中或在当前活动组的边侧打开时。 297 | "workbench.editor.revealIfOpen": false, 298 | 299 | // 控制命令面板中保留最近使用命令的数量。设置为 0 时禁用命令历史功能。 300 | "workbench.commandPalette.history": 50, 301 | 302 | // 控制是否在再次打开命令面板时恢复上一次的输入内容。 303 | "workbench.commandPalette.preserveInput": false, 304 | 305 | // 控制 Quick Open 是否应在失去焦点时自动关闭。 306 | "workbench.quickOpen.closeOnFocusLost": true, 307 | 308 | // 控制打开设置时是否打开显示所有默认设置的编辑器。 309 | "workbench.settings.openDefaultSettings": true, 310 | 311 | // 控制边栏的位置。它可显示在工作台的左侧或右侧。 312 | "workbench.sideBar.location": "left", 313 | 314 | // 控制面板的位置。它可显示在工作台的底部或右侧。 315 | "workbench.panel.location": "bottom", 316 | 317 | // 控制工作台底部状态栏的可见性。 318 | "workbench.statusBar.visible": true, 319 | 320 | // 控制工作台中活动栏的可见性。 321 | "workbench.activityBar.visible": true, 322 | 323 | // 控制文件被其他某些进程删除或重命名时是否应该自动关闭显示文件的编辑器。禁用此项会保持编辑器作为此类事件的脏文件打开。请注意,从应用程序内部进行删除操作会始终关闭编辑器,并且脏文件始终不会关闭以保存数据。 324 | "workbench.editor.closeOnFileDelete": true, 325 | 326 | // 控制工作台中字体的渲染方式 327 | // - default: 次像素平滑字体。将在大多数非 retina 显示器上显示最清晰的文字 328 | // - antialiased: 进行像素而不是次像素级别的字体平滑。可能会导致字体整体显示得更细 329 | // - none: 禁用字体平滑。将显示边缘粗糙、有锯齿的文字 330 | "workbench.fontAliasing": "default", 331 | 332 | // 使用三指横扫在打开的文件之间导航 333 | "workbench.editor.swipeToNavigate": false, 334 | 335 | // 启用后,当没有打开编辑器时将显示水印提示。 336 | "workbench.tips.enabled": true, 337 | 338 | // 指定工作台中使用的颜色主题。 339 | "workbench.colorTheme": "Default Dark+", 340 | 341 | // 指定在工作台中使用的图标主题,或指定 "null" 以不显示任何文件图标。 342 | "workbench.iconTheme": "vs-seti", 343 | 344 | // 覆盖当前所选颜色主题的颜色。 345 | "workbench.colorCustomizations": {}, 346 | 347 | 348 | // 控制是否应在新窗口中打开文件。 349 | // - default: 文件将在该文件的文件夹打开的窗口中打开,或在上一个活动窗口中打开,除非该文件通过平台或从查找程序(仅限 macOS)打开 350 | // - on: 文件将在新窗口中打开 351 | // - off: 文件将在该文件的文件夹打开的窗口中打开,或在上一个活动窗口中打开 352 | // 注意,可能仍会存在忽略此设置的情况(例如当使用 -new-window 或 -reuse-window 命令行选项时)。 353 | "window.openFilesInNewWindow": "off", 354 | 355 | // 控制文件夹应在新窗口中打开还是替换上一活动窗口。 356 | // - default: 文件夹将在新窗口中打开,除非文件是从应用程序内选取的(例如通过“文件”菜单) 357 | // - on: 文件夹将在新窗口中打开 358 | // - off: 文件夹将替换上一活动窗口 359 | // 注意,可能仍会存在忽略此设置的情况(例如当使用 -new-window 或 -reuse-window 命令行选项时)。 360 | "window.openFoldersInNewWindow": "default", 361 | 362 | // 控制重启后重新打开窗口的方式。选择 "none" 则永远在启动时打开一个空工作区,"one" 则重新打开最后使用的窗口,"folders" 则重新打开所有含有文件夹的窗口,"all" 则重新打开上次会话的所有窗口。 363 | "window.restoreWindows": "one", 364 | 365 | // 如果窗口已退出全屏模式,控制其是否应还原为全屏模式。 366 | "window.restoreFullscreen": false, 367 | 368 | // 调整窗口的缩放级别。原始大小是 0,每次递增(例如 1)或递减(例如 -1)表示放大或缩小 20%。也可以输入小数以便以更精细的粒度调整缩放级别。 369 | "window.zoomLevel": 0, 370 | 371 | // 根据活动编辑器控制窗口标题。变量基于上下文进行替换: 372 | // ${activeEditorShort}: 文件名 (如 myFile.txt) 373 | // ${activeEditorMedium}: 相对于工作区文件夹的文件路径 (如 myFolder/myFile.txt) 374 | // ${activeEditorLong}: 文件的完整路径 (如 /Users/Development/myProject/myFolder/myFile.txt) 375 | // ${folderName}: 文件所在工作区文件夹名称 (如 myFolder) 376 | // ${folderPath}: 文件所在工作区文件夹路径 (如 /Users/Development/myFolder) 377 | // ${rootName}: 工作区名称 (如 myFolder 或 myWorkspace) 378 | // ${rootPath}: 工作区路径 (如 /Users/Development/myWorkspace) 379 | // ${appName}: 如 VS Code 380 | // ${dirty}: 活动编辑器有更新时显示的更新指示器 381 | // ${separator}: 仅在被有值变量包围时显示的分隔符 (" - ") 382 | "window.title": "${activeEditorShort}${separator}${rootName}", 383 | 384 | // 控制在已有窗口时新打开窗口的尺寸。默认情况下,新窗口将以小尺寸在屏幕的中央打开。当设置为“inherit”时,新窗口将继承上一活动窗口的尺寸,设置为“maximized”时窗口将被最大化,设置为“fullscreen”时则变为全屏。请注意,此设置对第一个打开的窗口无效。第一个窗口总是会恢复关闭前的大小和位置。 385 | "window.newWindowDimensions": "default", 386 | 387 | // 控制关闭最后一个编辑器是否关闭整个窗口。此设置仅适用于不显示文件夹的窗口。 388 | "window.closeWhenEmpty": false, 389 | 390 | // 调整窗口标题栏的外观。更改需要在完全重启后才能应用。 391 | "window.titleBarStyle": "custom", 392 | 393 | // 394 | // 启用macOS Sierra窗口选项卡。请注意,更改需要完全重新启动程序才能生效。如果配置此选项,本机选项卡将禁用自定义标题栏样式。 395 | "window.nativeTabs": false, 396 | 397 | 398 | // 控制打开 Zen Mode 是否也会将工作台置于全屏模式。 399 | "zenMode.fullScreen": true, 400 | 401 | // 控制打开 Zen 模式是否也会隐藏工作台选项卡。 402 | "zenMode.hideTabs": true, 403 | 404 | // 控制打开 Zen 模式是否也会隐藏工作台底部的状态栏。 405 | "zenMode.hideStatusBar": true, 406 | 407 | // 控制打开 Zen 模式是否也会隐藏工作台左侧的活动栏。 408 | "zenMode.hideActivityBar": true, 409 | 410 | // 控制如果某窗口已退出 zen 模式,是否应还原到 zen 模式。 411 | "zenMode.restore": false, 412 | 413 | 414 | // 配置 glob 模式以在搜索中排除文件和文件夹。例如,文件资源管理器根据此设置决定文件或文件夹的显示和隐藏。 415 | "files.exclude": { 416 | "**/.git": true, 417 | "**/.svn": true, 418 | "**/.hg": true, 419 | "**/CVS": true, 420 | "**/.DS_Store": true 421 | }, 422 | 423 | // 配置语言的文件关联(如: "*.extension": "html")。这些关联的优先级高于已安装语言的默认关联。 424 | "files.associations": {}, 425 | 426 | // 读取和编写文件时使用的默认字符集编码。也可以根据语言配置此设置。 427 | "files.encoding": "utf8", 428 | 429 | // 如果启用,会在打开文件时尝试猜测字符集编码。也可以根据语言配置此设置。 430 | "files.autoGuessEncoding": false, 431 | 432 | // 默认行尾字符。使用 \n 表示 LF,\r\n 表示 CRLF。 433 | "files.eol": "\n", 434 | 435 | // 启用后,将在保存文件时剪裁尾随空格。 436 | "files.trimTrailingWhitespace": false, 437 | 438 | // 启用后,保存文件时在文件末尾插入一个最终新行。 439 | "files.insertFinalNewline": false, 440 | 441 | // 启用后,保存文件时将删除在最终新行后的所有新行。 442 | "files.trimFinalNewlines": false, 443 | 444 | // 控制已更新文件的自动保存。接受的值:“off”、"afterDelay”、“onFocusChange”(编辑器失去焦点)、“onWindowChange”(窗口失去焦点)。如果设置为“afterDelay”,则可在 "files.autoSaveDelay" 中配置延迟。 445 | "files.autoSave": "off", 446 | 447 | // 控制在多少毫秒后自动保存更改过的文件。仅在“files.autoSave”设置为“afterDelay”时适用。 448 | "files.autoSaveDelay": 1000, 449 | 450 | // 配置文件路径的 glob 模式以从文件监视排除。模式必须在绝对路径上匹配(例如 ** 前缀或完整路径需正确匹配)。更改此设置需要重启。如果在启动时遇到 Code 消耗大量 CPU 时间,则可以排除大型文件夹以减少初始加载。 451 | "files.watcherExclude": { 452 | "**/.git/objects/**": true, 453 | "**/.git/subtree-cache/**": true, 454 | "**/node_modules/**": true 455 | }, 456 | 457 | // 控制是否在会话间记住未保存的文件,以允许在退出编辑器时跳过保存提示。 458 | "files.hotExit": "onExit", 459 | 460 | // 使用新的试验文件观察程序。 461 | "files.useExperimentalFileWatcher": false, 462 | 463 | // 分配给新文件的默认语言模式。 464 | "files.defaultLanguage": "", 465 | 466 | 467 | // 在“打开的编辑器”窗格中显示的编辑器数量。将其设置为 0 可隐藏窗格。 468 | "explorer.openEditors.visible": 9, 469 | 470 | // 控制打开的编辑器部分的高度是否应动态适应元素数量。 471 | "explorer.openEditors.dynamicHeight": true, 472 | 473 | // 控制资源管理器是否应在打开文件时自动显示并选择它们。 474 | "explorer.autoReveal": true, 475 | 476 | // 控制资源管理器是否应该允许通过拖放移动文件和文件夹。 477 | "explorer.enableDragAndDrop": true, 478 | 479 | // 控制在资源管理器内拖放移动文件或文件夹时是否进行确认。 480 | "explorer.confirmDragAndDrop": true, 481 | 482 | // 控制资源管理器是否应在删除文件到回收站时进行确认。 483 | "explorer.confirmDelete": true, 484 | 485 | // 控制资源管理器文件和文件夹的排列顺序。除了默认排列顺序,你也可以设置为 "mixed" (文件和文件夹一起排序)、"type" (按文件类型排)、"modified" (按最后修改日期排)或是 "filesFirst" (将文件排在文件夹前)。 486 | "explorer.sortOrder": "default", 487 | 488 | // 控制文件修饰是否用颜色。 489 | "explorer.decorations.colors": true, 490 | 491 | // 控制文件修饰是否用徽章。 492 | "explorer.decorations.badges": true, 493 | 494 | 495 | // 配置 glob 模式以在搜索中排除文件和文件夹。从 files.exclude 设置中继承所有 glob 模式。 496 | "search.exclude": { 497 | "**/node_modules": true, 498 | "**/bower_components": true 499 | }, 500 | 501 | // 控制是否在文本和文件搜索中使用 ripgrep 502 | "search.useRipgrep": true, 503 | 504 | // 控制在新工作区中搜索文本时是否默认使用 .gitignore 和 .ignore 文件。 505 | "search.useIgnoreFilesByDefault": false, 506 | 507 | // 控制搜索文件时是否使用 .gitignore 和 .ignore 文件。 508 | "search.useIgnoreFiles": false, 509 | 510 | // 配置为在 Quick Open 文件结果中包括全局符号搜索的结果。 511 | "search.quickOpen.includeSymbols": false, 512 | 513 | // 控制是否在搜索中跟踪符号链接。 514 | "search.followSymlinks": true, 515 | 516 | 517 | // 要使用的代理设置。如果尚未设置,则将从 http_proxy 和 https_proxy 环境变量获取 518 | "http.proxy": "", 519 | 520 | // 是否应根据提供的 CA 列表验证代理服务器证书。 521 | "http.proxyStrictSSL": true, 522 | 523 | // 要作为每个网络请求的 "Proxy-Authorization" 标头发送的值。 524 | "http.proxyAuthorization": null, 525 | 526 | 527 | // 配置是否从更新通道接收自动更新。更改后需要重启。 528 | "update.channel": "default", 529 | 530 | 531 | // 控制按键的调度逻辑以使用“keydown.code”(推荐) 或“keydown.keyCode”。 532 | "keyboard.dispatch": "code", 533 | 534 | 535 | // 启用/禁用默认 HTML 格式化程序 536 | "html.format.enable": true, 537 | 538 | // 每行最大字符数(0 = 禁用)。 539 | "html.format.wrapLineLength": 120, 540 | 541 | // 以逗号分隔的标记列表不应重设格式。"null" 默认为所有列于 https://www.w3.org/TR/html5/dom.html#phrasing-content 的标记。 542 | "html.format.unformatted": "wbr", 543 | 544 | // 以逗号分隔的标记列表,不应在其中重新设置内容的格式。"null" 默认为 "pre" 标记。 545 | "html.format.contentUnformatted": "pre,code,textarea", 546 | 547 | // 缩进
和 部分。 548 | "html.format.indentInnerHtml": false, 549 | 550 | // 是否要保留元素前面的现有换行符。仅适用于元素前,不适用于标记内或文本。 551 | "html.format.preserveNewLines": true, 552 | 553 | // 要保留在一个区块中的换行符的最大数量。对于无限制使用 "null"。 554 | "html.format.maxPreserveNewLines": null, 555 | 556 | // 格式和缩进 {{#foo}} 和 {{/foo}}。 557 | "html.format.indentHandlebars": false, 558 | 559 | // 以新行结束。 560 | "html.format.endWithNewline": false, 561 | 562 | // 标记列表,以逗号分隔,其前应有额外新行。"null" 默认为“标头、正文、/html”。 563 | "html.format.extraLiners": "head, body, /html", 564 | 565 | // 对属性进行换行。 566 | "html.format.wrapAttributes": "auto", 567 | 568 | // 配置内置 HTML 语言支持是否建议 Angular V1 标记和属性。 569 | "html.suggest.angular1": true, 570 | 571 | // 配置内置 HTML 语言支持是否建议 Ionic 标记、属性和值。 572 | "html.suggest.ionic": true, 573 | 574 | // 配置内置 HTML 语言支持是否建议 HTML5 标记、属性和值。 575 | "html.suggest.html5": true, 576 | 577 | // 配置内置的 HTML 语言支持是否对嵌入的脚本进行验证。 578 | "html.validate.scripts": true, 579 | 580 | // 配置内置的 HTML 语言支持是否对嵌入的样式进行验证。 581 | "html.validate.styles": true, 582 | 583 | // 启用/禁用 HTML 标记的自动关闭。 584 | "html.autoClosingTags": true, 585 | 586 | // 跟踪 VS Code 与 HTML 语言服务器之间的通信。 587 | "html.trace.server": "off", 588 | 589 | 590 | // 将当前项目中的 JSON 文件与架构关联起来 591 | "json.schemas": [], 592 | 593 | // 启用/禁用默认 JSON 格式化程序(需要重启) 594 | "json.format.enable": true, 595 | 596 | // 跟踪 VS Code 与 JSON 语言服务器之间的通信。 597 | "json.trace.server": "off", 598 | 599 | 600 | // 要在 Markdown 预览中使用的 CSS 样式表的 URL 或本地路径列表。相对路径被解释为相对于资源管理器中打开的文件夹。如果没有任何打开的文件夹,则会被解释为相对于 Markdown 文件的位置。所有的 "\" 需写为 "\\"。 601 | "markdown.styles": [], 602 | 603 | // 设置如何在 Markdown 预览中呈现 YAML 扉页。“隐藏”会删除扉页。否则,扉页则被视为 Markdown 内容。 604 | "markdown.previewFrontMatter": "hide", 605 | 606 | // 设置换行符如何在 markdown 预览中呈现。将其设置为 "true" 会为每一个新行创建一个