├── .eslintignore ├── screenshot ├── home.gif ├── home2.gif ├── sign.gif ├── write.gif ├── detail.gif ├── follow.gif └── introduce.gif ├── app.js ├── .gitattributes ├── .idea └── vcs.xml ├── config ├── config.unittest.js ├── plugin.js └── config.default.js ├── .travis.yml ├── .gitignore ├── app ├── extend │ ├── helper.js │ └── context.js ├── service │ ├── user.js │ ├── login.js │ ├── articleType.js │ ├── comment.js │ └── article.js ├── controller │ ├── admin │ │ └── login.js │ ├── page.js │ ├── client │ │ └── login.js │ ├── users.js │ ├── articleType.js │ └── article.js ├── middleware │ └── auth.js ├── model │ ├── articleTypes.js │ ├── commentLove.js │ ├── articleBack.js │ ├── users.js │ ├── commentReply.js │ ├── comment.js │ └── article.js └── router.js ├── .eslintrc ├── .sequelizerc ├── appveyor.yml ├── database ├── config.json ├── migrations │ ├── 20181217130011-init-comment-loves.js │ ├── 20181203033901-init-article-types.js │ ├── 20181216033631-init-comments.js │ ├── 20181216033658-init-comment-replys.js │ ├── 20181213062911-init-articles-back.js │ ├── 20181129051931-init-users.js │ └── 20181130061343-init-articles.js └── create.md ├── .autod.conf.js ├── test └── app │ └── controller │ └── home.test.js ├── package.json ├── README.md └── db └── db.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /screenshot/home.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/home.gif -------------------------------------------------------------------------------- /screenshot/home2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/home2.gif -------------------------------------------------------------------------------- /screenshot/sign.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/sign.gif -------------------------------------------------------------------------------- /screenshot/write.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/write.gif -------------------------------------------------------------------------------- /screenshot/detail.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/detail.gif -------------------------------------------------------------------------------- /screenshot/follow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/follow.gif -------------------------------------------------------------------------------- /screenshot/introduce.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jieyuanfei/jianshu-egg-api/HEAD/screenshot/introduce.gif -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.beforeStart(async () => { 3 | // 应用会等待这个函数执行完成才启动 4 | console.log('=====初始化======') 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=javascript 2 | *.css linguist-language=javascript 3 | *.scss linguist-language=javascript 4 | *.html linguist-language=javascript 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /config/config.unittest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.sequelize = { 4 | dialect: 'mysql', // support: mysql, mariadb, postgres, mssql 5 | database: 'egg-sequelize-example-unittest', 6 | }; 7 | -------------------------------------------------------------------------------- /.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 | config/ 10 | run/ 11 | .DS_Store 12 | *.sw* 13 | *.un~ 14 | config/config.default.js 15 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | parseInt(string) { 5 | if (typeof string === 'number') return string; 6 | if (!string) return string; 7 | return parseInt(string) || 0; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg", 3 | "rules": { 4 | "strict": 0, 5 | "linebreak-style": 0, 6 | "semi": 0, 7 | "comma-dangle": 0, 8 | "object-shorthand": 0, 9 | "prefer-const": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | config: path.join(__dirname, 'database/config.json'), 7 | 'migrations-path': path.join(__dirname, 'database/migrations'), 8 | 'seeders-path': path.join(__dirname, 'database/seeders'), 9 | }; 10 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // had enabled by egg 4 | // exports.static = true; 5 | // module.exports = { 6 | // mongoose: { 7 | // enable: true, 8 | // package: 'egg-mongoose' 9 | // } 10 | // } 11 | exports.sequelize = { 12 | enable: true, 13 | package: 'egg-sequelize', 14 | }; 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 | -------------------------------------------------------------------------------- /database/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "root", 4 | "password": "123", 5 | "database": "egg-sequelize-example-dev", 6 | "host": "127.0.0.1", 7 | "dialect": "mysql" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": "123", 12 | "database": "egg-sequelize-example-unittest", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/service/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class User extends Service { 6 | 7 | 8 | async find(id) { 9 | const user = await this.ctx.model.Users.findById(id); 10 | if (!user) { 11 | return { 12 | code: 404, 13 | msg: '用户不存在' 14 | } 15 | } 16 | return { 17 | code: 0, 18 | data: user 19 | }; 20 | } 21 | } 22 | 23 | module.exports = User; 24 | -------------------------------------------------------------------------------- /app/controller/admin/login.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | class LoginController extends Controller { 3 | // 获取验证码 4 | async getCaptcha() { 5 | const { ctx } = this 6 | const captcha = ctx.service.login.genCaptcha() 7 | // 把生成的验证码文本信息(如:t8ec),存入session,以待验证 8 | ctx.session.code = captcha.text 9 | // 将生成的验证码svg图片返回给前端 10 | ctx.body = captcha.data 11 | } 12 | } 13 | 14 | module.exports = LoginController 15 | -------------------------------------------------------------------------------- /app/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | module.exports = () => { 3 | return async function auth(ctx, next) { 4 | try { 5 | let decode = jwt.verify(ctx.get('Authorization'), ctx.app.config.jwt.cert) 6 | ctx.userId = decode.id 7 | } catch (err) { 8 | console.log('登录权限获取失败' + err) 9 | ctx.error(401, '授权失败,请重新登录') 10 | return 11 | } 12 | await next() // 这里因为next之后的操作是异步的所以需要加 await 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /.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, egasdfg') 19 | .expect(200); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /database/migrations/20181217130011-init-comment-loves.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 t_comment_loves 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE } = Sequelize; 7 | await queryInterface.createTable('t_comment_loves', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | comment_id: INTEGER, 10 | user_id: INTEGER, 11 | created_at: DATE, 12 | updated_at: DATE 13 | }); 14 | }, 15 | // 在执行数据库降级时调用的函数,删除 t_comment_loves 表 16 | down: async queryInterface => { 17 | await queryInterface.dropTable('t_comment_loves'); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /database/migrations/20181203033901-init-article-types.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 users 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_article_types', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | type_name: STRING(255), 10 | user_id: INTEGER, 11 | delete: INTEGER(2), 12 | created_at: DATE, 13 | updated_at: DATE, 14 | }); 15 | }, 16 | // 在执行数据库降级时调用的函数,删除 users 表 17 | down: async queryInterface => { 18 | await queryInterface.dropTable('t_article_types'); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /app/extend/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | result(data = {}, success = true, code = 0, msg = '') { 5 | let resultJson = { 6 | success: success, 7 | data: data, 8 | code: code, 9 | msg: msg, 10 | } 11 | this.body = resultJson; 12 | }, 13 | success(data = {}) { 14 | let resultJson = { 15 | success: true, 16 | data: data, 17 | code: 0, 18 | msg: '', 19 | } 20 | this.body = resultJson; 21 | }, 22 | error(code = 0, msg = '') { 23 | let resultJson = { 24 | success: false, 25 | data: {}, 26 | code: code, 27 | msg: msg, 28 | } 29 | this.body = resultJson; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /database/migrations/20181216033631-init-comments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 t_comments 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_comments', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | article_id: INTEGER, 10 | user_id: INTEGER, 11 | user_name: STRING, 12 | user_header_url: STRING, 13 | content: STRING(600), 14 | love_num: INTEGER, 15 | created_at: DATE, 16 | updated_at: DATE, 17 | }); 18 | }, 19 | // 在执行数据库降级时调用的函数,删除 t_comments 表 20 | down: async queryInterface => { 21 | await queryInterface.dropTable('t_comments'); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/20181216033658-init-comment-replys.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 t_comment_replys 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_comment_replys', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | comment_id: INTEGER, 10 | reply_id: INTEGER, 11 | from_user_id: INTEGER, 12 | from_user_name: STRING, 13 | to_user_id: INTEGER, 14 | to_user_name: STRING, 15 | content: STRING(600), 16 | created_at: DATE, 17 | updated_at: DATE, 18 | }); 19 | }, 20 | // 在执行数据库降级时调用的函数,删除 t_comment_replys 表 21 | down: async queryInterface => { 22 | await queryInterface.dropTable('t_comment_replys'); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = appInfo => { 4 | return { 5 | keys: appInfo.name + '_153332185447_3632', 6 | cluster: { 7 | listen: { 8 | path: '', 9 | port: 7002, 10 | hostname: '127.0.0.1', 11 | } 12 | }, 13 | sequelize: { 14 | dialect: 'mysql', // support: mysql, mariadb, postgres, mssql 15 | database: '', 16 | host: '', 17 | username: '', 18 | password: '', 19 | port: 3306, 20 | }, 21 | user: { // 初始化管理员的账号 22 | userName: 'admin', 23 | password: 'admin', 24 | }, 25 | session: { 26 | maxAge: 3600 * 1000, 27 | }, 28 | jwt: { 29 | cert: 'huanggegehaoshuai' // jwt秘钥 30 | }, 31 | qiniu: { // 这里填写你七牛的Access Key和Secret Key 32 | ak: '', 33 | sk: '' 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/20181213062911-init-articles-back.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 t_article_back 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_article_backs', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | article_id: INTEGER, 10 | type_id: INTEGER, 11 | title: STRING(255), 12 | text: STRING(255), 13 | images: STRING(1500), // 最多保存5张图片 14 | content: STRING(10000), 15 | article_num: INTEGER, 16 | status: INTEGER, 17 | created_at: DATE, 18 | updated_at: DATE, 19 | }); 20 | }, 21 | // 在执行数据库降级时调用的函数,删除 t_article_back 表 22 | down: async queryInterface => { 23 | await queryInterface.dropTable('t_article_backs'); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /database/migrations/20181129051931-init-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 users 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_users', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | username: STRING(30), 10 | password: STRING(255), 11 | header_url: STRING(255), 12 | user_email: STRING(100), 13 | phone: STRING(11), 14 | phone_validate: INTEGER(2), 15 | edit_type: INTEGER(2), 16 | language: INTEGER(2), 17 | receive_msg: INTEGER(2), 18 | notice: INTEGER(2), 19 | created_at: DATE, 20 | updated_at: DATE, 21 | }); 22 | }, 23 | // 在执行数据库降级时调用的函数,删除 users 表 24 | down: async queryInterface => { 25 | await queryInterface.dropTable('t_users'); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /app/controller/page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | const fs = require('fs') 5 | const util = require('util') 6 | const path = require('path') 7 | const readFilePromise = util.promisify(fs.readFile) 8 | class PageController extends Controller { 9 | /** 10 | * 打包后可以通过 11 | * http://rootUrl/ 和 http://rootUrl/admin 来访问前台可后台 12 | * 在开发阶段直接通过webpack的devserver来访问,接口通过proxyTable来代理 13 | */ 14 | async index() { 15 | const { ctx } = this 16 | ctx.response.type = 'html' 17 | let page = await readFilePromise(path.resolve(__dirname, '../public/client/dist/index.html')) 18 | ctx.body = page 19 | } 20 | async admin() { 21 | const { ctx } = this 22 | ctx.response.type = 'html' 23 | let page = await readFilePromise(path.resolve(__dirname, '../public/admin/dist/index.html')) 24 | ctx.body = page 25 | } 26 | } 27 | 28 | module.exports = PageController; 29 | 30 | -------------------------------------------------------------------------------- /database/migrations/20181130061343-init-articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | // 在执行数据库升级时调用的函数,创建 t_article 表 5 | up: async (queryInterface, Sequelize) => { 6 | const { INTEGER, DATE, STRING } = Sequelize; 7 | await queryInterface.createTable('t_articles', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | title: STRING(255), 10 | text: STRING(255), 11 | images: STRING(1500), // 最多保存5张图片 12 | content: STRING(10000), 13 | user_id: INTEGER, 14 | type_id: INTEGER, 15 | article_num: INTEGER(2), 16 | ready_num: INTEGER(2), 17 | like_num: INTEGER(2), 18 | comment_num: INTEGER(2), 19 | status: INTEGER(2), 20 | created_at: DATE, 21 | updated_at: DATE, 22 | }); 23 | }, 24 | // 在执行数据库降级时调用的函数,删除 t_article 表 25 | down: async queryInterface => { 26 | await queryInterface.dropTable('t_articles'); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /app/model/articleTypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | const ArticleTypes = app.model.define('t_article_types', { 7 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 8 | type_name: STRING(255), 9 | user_id: INTEGER, 10 | delete: INTEGER(2), 11 | created_at: DATE, 12 | updated_at: DATE, 13 | }); 14 | 15 | ArticleTypes.findOneByElement = async function(params) { 16 | if (typeof params !== 'object') { 17 | return {}; 18 | } 19 | let data = await this.findOne({ 20 | where: params, 21 | }) 22 | if (data === null) { 23 | return data; 24 | } 25 | return data.dataValues; 26 | 27 | }; 28 | ArticleTypes.findColumnByElement = async function(Column, params) { 29 | if (typeof params !== 'object') { 30 | return {}; 31 | } 32 | return await this.findOne({ 33 | where: params, 34 | }); 35 | 36 | }; 37 | return ArticleTypes; 38 | }; 39 | -------------------------------------------------------------------------------- /app/model/commentLove.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { INTEGER, DATE } = app.Sequelize; 5 | 6 | // 第一个是别名 第二个是表名 7 | const CommentLove = app.model.define('t_comment_loves', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | comment_id: INTEGER, 10 | user_id: INTEGER, 11 | created_at: DATE, 12 | updated_at: DATE 13 | }, { 14 | freezeTableName: true, 15 | tableName: 't_comment_loves' 16 | }); 17 | 18 | CommentLove.findOneByElement = async function(params) { 19 | if (typeof params !== 'object') { 20 | return {}; 21 | } 22 | let data = await this.findOne({ 23 | where: params, 24 | }) 25 | if (data === null) { 26 | return data; 27 | } 28 | return data.dataValues; 29 | 30 | }; 31 | CommentLove.findColumnByElement = async function(Column, params) { 32 | if (typeof params !== 'object') { 33 | return {}; 34 | } 35 | return await this.findOne({ 36 | where: params, 37 | }); 38 | 39 | }; 40 | return CommentLove; 41 | }; 42 | -------------------------------------------------------------------------------- /app/model/articleBack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | // 添加文章备份表 7 | const ArticleBack = app.model.define('t_article_backs', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | article_id: INTEGER, 10 | type_id: INTEGER, 11 | title: STRING(255), 12 | text: STRING(255), 13 | images: STRING(1500), // 最多保存5张图片 14 | content: STRING(10000), 15 | article_num: INTEGER, 16 | status: INTEGER, 17 | created_at: DATE, 18 | updated_at: DATE, 19 | }); 20 | 21 | ArticleBack.findOneByElement = async function(params) { 22 | if (typeof params !== 'object') { 23 | return {}; 24 | } 25 | let data = await this.findOne({ 26 | where: params, 27 | }) 28 | if (data === null) { 29 | return data; 30 | } 31 | return data.dataValues; 32 | 33 | }; 34 | ArticleBack.findColumnByElement = async function(Column, params) { 35 | if (typeof params !== 'object') { 36 | return {}; 37 | } 38 | return await this.findOne({ 39 | where: params, 40 | }); 41 | 42 | }; 43 | return ArticleBack; 44 | }; 45 | -------------------------------------------------------------------------------- /app/model/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | const Users = app.model.define('t_users', { 7 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 8 | username: STRING(30), 9 | password: STRING(255), 10 | header_url: STRING(255), 11 | user_email: STRING(100), 12 | phone: STRING(11), 13 | phone_validate: INTEGER(2), 14 | edit_type: INTEGER(2), 15 | language: INTEGER(2), 16 | receive_msg: INTEGER(2), 17 | notice: INTEGER(2), 18 | created_at: DATE, 19 | updated_at: DATE, 20 | }); 21 | 22 | Users.findOneByElement = async function(params) { 23 | if (typeof params !== 'object') { 24 | return {}; 25 | } 26 | let data = await this.findOne({ 27 | where: params, 28 | }) 29 | if (data === null) { 30 | return data; 31 | } 32 | return data.dataValues; 33 | 34 | }; 35 | Users.findColumnByElement = async function(Column, params) { 36 | if (typeof params !== 'object') { 37 | return {}; 38 | } 39 | return await this.findOne({ 40 | where: params, 41 | }); 42 | 43 | }; 44 | return Users; 45 | }; 46 | -------------------------------------------------------------------------------- /app/model/commentReply.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | // 第一个是别名 第二个是表名 7 | const CommentReply = app.model.define('t_comment_replys', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | comment_id: INTEGER, 10 | reply_id: INTEGER, 11 | from_user_id: INTEGER, 12 | from_user_name: STRING, 13 | to_user_id: INTEGER, 14 | to_user_name: STRING, 15 | content: STRING(600), 16 | created_at: DATE, 17 | updated_at: DATE, 18 | }, { 19 | freezeTableName: true, 20 | tableName: 't_comment_replys' 21 | }); 22 | 23 | CommentReply.findOneByElement = async function(params) { 24 | if (typeof params !== 'object') { 25 | return {}; 26 | } 27 | let data = await this.findOne({ 28 | where: params, 29 | }) 30 | if (data === null) { 31 | return data; 32 | } 33 | return data.dataValues; 34 | 35 | }; 36 | CommentReply.findColumnByElement = async function(Column, params) { 37 | if (typeof params !== 'object') { 38 | return {}; 39 | } 40 | return await this.findOne({ 41 | where: params, 42 | }); 43 | 44 | }; 45 | return CommentReply; 46 | }; 47 | -------------------------------------------------------------------------------- /app/controller/client/login.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | class LoginController extends Controller { 3 | // 获取验证码 4 | async getCaptcha() { 5 | const { ctx } = this 6 | const captcha = ctx.service.login.genCaptcha() 7 | // 把生成的验证码文本信息(如:t8ec),存入session,以待验证 8 | ctx.session.code = captcha.text 9 | // 将生成的验证码svg图片返回给前端 10 | ctx.body = captcha.data 11 | } 12 | // 登录 13 | async login() { 14 | const { ctx } = this 15 | const { username, password } = ctx.request.body 16 | // let isCaptchaVali = ctx.service.login.checkCaptcha(code) 17 | // if (!isCaptchaVali) { 18 | // ctx.error(1, '验证码不正确') 19 | // return 20 | // } 21 | // 验证码正确则继续登录操作 22 | const userData = await ctx.service.login.login({ username, password }) 23 | if (userData.err === 1 || userData.err === 2) { 24 | ctx.error(2, '用户名不存在或密码错误') 25 | return 26 | } 27 | let data = { 28 | username: userData.user.username, 29 | uid: userData.user.id, 30 | header_url: userData.user.header_url, 31 | token: userData.token 32 | } 33 | ctx.success(data) 34 | } 35 | // 注册 36 | async register() { 37 | const { ctx } = this 38 | // 暂时未开发 39 | ctx.success({}) 40 | } 41 | } 42 | 43 | module.exports = LoginController 44 | -------------------------------------------------------------------------------- /app/model/comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | // 第一个是别名 第二个是表名 7 | const Comment = app.model.define('t_comments', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | article_id: INTEGER, 10 | user_id: INTEGER, 11 | user_name: STRING, 12 | user_header_url: STRING, 13 | content: STRING(600), 14 | love_num: INTEGER, 15 | created_at: DATE, 16 | updated_at: DATE, 17 | }, { 18 | freezeTableName: true, 19 | tableName: 't_comments' 20 | }); 21 | 22 | Comment.findOneByElement = async function(params) { 23 | if (typeof params !== 'object') { 24 | return {}; 25 | } 26 | let data = await this.findOne({ 27 | where: params, 28 | }) 29 | if (data === null) { 30 | return data; 31 | } 32 | return data.dataValues; 33 | 34 | }; 35 | Comment.findColumnByElement = async function(Column, params) { 36 | if (typeof params !== 'object') { 37 | return {}; 38 | } 39 | return await this.findOne({ 40 | where: params, 41 | }); 42 | 43 | }; 44 | Comment.associate = function() { 45 | app.model.Comment.hasMany(app.model.CommentReply, { foreignKey: 'comment_id', targetKey: 'id' }); 46 | }; 47 | return Comment; 48 | }; 49 | -------------------------------------------------------------------------------- /app/controller/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class UserController extends Controller { 6 | async index() { 7 | const ctx = this.ctx; 8 | const query = { 9 | limit: ctx.helper.parseInt(ctx.query.limit), 10 | offset: ctx.helper.parseInt(ctx.query.offset), 11 | }; 12 | ctx.success(await ctx.service.user.list(query)) 13 | } 14 | async test() { 15 | const ctx = this.ctx; 16 | let username = 'jyf1', 17 | password = '123'; 18 | ctx.success(await ctx.service.login.login({ username, password })) 19 | } 20 | 21 | async show() { 22 | const ctx = this.ctx; 23 | ctx.body = await ctx.service.user.find(ctx.helper.parseInt(ctx.params.id)); 24 | } 25 | 26 | async create() { 27 | const ctx = this.ctx; 28 | const user = await ctx.service.user.create(ctx.request.body); 29 | ctx.status = 201; 30 | ctx.body = user; 31 | } 32 | 33 | async update() { 34 | const ctx = this.ctx; 35 | const id = ctx.helper.parseInt(ctx.params.id); 36 | const body = ctx.request.body; 37 | ctx.body = await ctx.service.user.update({ id, updates: body }); 38 | } 39 | 40 | async destroy() { 41 | const ctx = this.ctx; 42 | const id = ctx.helper.parseInt(ctx.params.id); 43 | await ctx.service.user.del(id); 44 | ctx.status = 200; 45 | } 46 | } 47 | 48 | module.exports = UserController; 49 | -------------------------------------------------------------------------------- /app/model/article.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { STRING, INTEGER, DATE } = app.Sequelize; 5 | 6 | // 第一个是别名 第二个是表名 7 | const Article = app.model.define('t_articles', { 8 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 9 | title: STRING(255), 10 | text: STRING(255), 11 | images: STRING(1500), // 最多保存5张图片 12 | content: STRING(10000), 13 | user_id: INTEGER, 14 | type_id: INTEGER, 15 | article_num: INTEGER(2), 16 | ready_num: INTEGER(2), 17 | like_num: INTEGER(2), 18 | comment_num: INTEGER(2), 19 | status: INTEGER(2), // 0 发布 1 草稿 2 私密 3 删除 20 | created_at: DATE, 21 | updated_at: DATE 22 | }, { 23 | freezeTableName: true, 24 | tableName: 't_articles' 25 | }); 26 | 27 | Article.findOneByElement = async function(params) { 28 | if (typeof params !== 'object') { 29 | return {}; 30 | } 31 | let data = await this.findOne({ 32 | where: params, 33 | }) 34 | if (data === null) { 35 | return data; 36 | } 37 | return data.dataValues; 38 | 39 | }; 40 | Article.findColumnByElement = async function(Column, params) { 41 | if (typeof params !== 'object') { 42 | return {}; 43 | } 44 | return await this.findOne({ 45 | where: params, 46 | }); 47 | 48 | }; 49 | Article.associate = function() { 50 | app.model.Article.belongsTo(app.model.Users, { foreignKey: 'user_id', targetKey: 'id' }); 51 | }; 52 | return Article; 53 | }; 54 | -------------------------------------------------------------------------------- /app/service/login.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | const jwt = require('jsonwebtoken') 3 | const svgCaptcha = require('svg-captcha') 4 | class LoginService extends Service { 5 | // 生成验证码 6 | genCaptcha() { 7 | return svgCaptcha.create({ 8 | width: 85, 9 | height: 38 10 | }) 11 | } 12 | // 检查验证码是否正确 13 | checkCaptcha(code) { 14 | const { ctx } = this 15 | code = code.toLowerCase() 16 | let sessCode = ctx.session.code ? ctx.session.code.toLowerCase() : null // 拿到之前存在session中的验证码 17 | // 进行验证 18 | if (code === sessCode) { 19 | // 成功后验证码作废 20 | ctx.session.code = null 21 | } 22 | return code === sessCode 23 | } 24 | // 登录操作 25 | async login({ username, password }) { 26 | const { ctx, app } = this 27 | const userData = await ctx.model.Users.findOneByElement({ 28 | username: username 29 | }) 30 | let user = { 31 | user: {}, 32 | token: '', 33 | err: 0 // 0 登录成功 1 用户不存在 2 用户密码错误 34 | } 35 | // 用户不存在 36 | if (!userData) { 37 | user.err = 1; 38 | return user; 39 | } 40 | if (userData.password !== password) { 41 | user.err = 2; 42 | return user; 43 | } 44 | 45 | // 找到则以用户id 生成token 46 | const token = jwt.sign({ 47 | id: userData.id 48 | }, app.config.jwt.cert, { 49 | expiresIn: '10h' // token过期时间 50 | }) 51 | user.user = userData; 52 | user.token = token; 53 | return user; 54 | } 55 | } 56 | 57 | module.exports = LoginService 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "dependencies": { 7 | "cheerio": "^1.0.0-rc.2", 8 | "egg": "^2.2.1", 9 | "egg-mongoose": "^3.1.0", 10 | "egg-scripts": "^2.11.0", 11 | "egg-sequelize": "^4.2.0", 12 | "jsonwebtoken": "^8.3.0", 13 | "mysql2": "^1.6.4", 14 | "qiniu": "^7.2.1", 15 | "svg-captcha": "^1.3.11" 16 | }, 17 | "devDependencies": { 18 | "autod": "^3.0.1", 19 | "autod-egg": "^1.0.0", 20 | "egg-bin": "^4.3.5", 21 | "egg-ci": "^1.8.0", 22 | "egg-mock": "^3.14.0", 23 | "eslint": "^4.11.0", 24 | "eslint-config-egg": "^6.0.0", 25 | "factory-girl": "^5.0.2", 26 | "lodash": "^4.17.10", 27 | "sequelize-cli": "^4.1.1", 28 | "webstorm-disable-index": "^1.2.0" 29 | }, 30 | "engines": { 31 | "node": ">=8.9.0" 32 | }, 33 | "scripts": { 34 | "start": "egg-scripts start --daemon --title=egg-server-demo", 35 | "stop": "egg-scripts stop --title=egg-server-demo", 36 | "dev": "egg-bin dev", 37 | "debug": "egg-bin debug", 38 | "test": "npm run lint -- --fix && npm run test-local", 39 | "test-local": "egg-bin test", 40 | "cov": "egg-bin cov", 41 | "lint": "eslint .", 42 | "ci": "npm run lint && npm run cov", 43 | "autod": "autod", 44 | "sequelize": "sequelize --" 45 | }, 46 | "ci": { 47 | "version": "8" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "" 52 | }, 53 | "author": "wade", 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /app/controller/articleType.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | class ArticleTypeController extends Controller { 3 | // 获取所有文章列表 4 | async getListByUserId() { 5 | const { ctx } = this 6 | let data = await ctx.service.articleType.list() 7 | ctx.success(data) 8 | } 9 | // 添加新文集 10 | async create() { 11 | const ctx = this.ctx; 12 | const { typeName } = ctx.request.body 13 | let query = { 14 | userId: ctx.userId, 15 | typeName: typeName 16 | } 17 | const articleType = await ctx.service.articleType.create(query); 18 | if (articleType.code === 0) { 19 | ctx.success(articleType.data) 20 | } else { 21 | ctx.error(articleType.code, articleType.msg) 22 | } 23 | } 24 | 25 | // 删除文集 26 | async del() { 27 | const ctx = this.ctx; 28 | const { id } = ctx.request.body 29 | const articleType = await ctx.service.articleType.del(id); 30 | if (articleType.code === 0) { 31 | ctx.success(articleType.data) 32 | } else { 33 | ctx.error(articleType.code, articleType.msg) 34 | } 35 | } 36 | 37 | // 修改文集 38 | async update() { 39 | const ctx = this.ctx; 40 | const { id, typeName } = ctx.request.body 41 | if (id == null || typeName.length === 0) { 42 | ctx.error(1, '文集名称不能为空') 43 | } 44 | let query = { 45 | id: id, 46 | type_name: typeName 47 | } 48 | const articleType = await ctx.service.articleType.update(query); 49 | if (articleType.code === 0) { 50 | ctx.success(articleType.data) 51 | } else { 52 | ctx.error(articleType.code, articleType.msg) 53 | } 54 | } 55 | 56 | } 57 | 58 | module.exports = ArticleTypeController 59 | -------------------------------------------------------------------------------- /app/service/articleType.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class ArticleType extends Service { 6 | async list() { 7 | let user_id = this.ctx.userId; 8 | return this.ctx.model.ArticleTypes.findAndCountAll({ 9 | where: { user_id: user_id }, 10 | order: [[ 'created_at', 'asc' ]], 11 | }); 12 | } 13 | 14 | async find(id) { 15 | const articleTypes = await this.ctx.model.ArticleTypes.findById(id); 16 | if (!articleTypes) { 17 | this.ctx.error(404, '不存在文集类型'); 18 | } 19 | return articleTypes; 20 | } 21 | 22 | async create(query) { 23 | const { ctx } = this; 24 | let option = { 25 | type_name: query.typeName, 26 | user_id: query.userId, 27 | delete: '0' 28 | } 29 | let typeData = await ctx.model.ArticleTypes.findOneByElement(option) 30 | if (typeData) { 31 | return { 32 | code: 1, 33 | msg: '不能创建同一个文集名称' 34 | } 35 | } 36 | let data = await ctx.model.ArticleTypes.create(option); 37 | if (!data) { 38 | return { 39 | code: 1, 40 | msg: '插入失败' 41 | } 42 | } 43 | return { 44 | code: 0, 45 | data: data.dataValues 46 | } 47 | 48 | } 49 | 50 | async update(query) { 51 | const articleTypes = await this.ctx.model.ArticleTypes.findById(query.id); 52 | if (!articleTypes) { 53 | return { 54 | code: 404, 55 | msg: '不存在文集类型' 56 | } 57 | } 58 | let user_id = this.ctx.userId 59 | const articleTypes2 = await this.ctx.model.ArticleTypes.findAndCountAll({ where: { type_name: query.type_name ,user_id: user_id} }); 60 | if (articleTypes2.count > 0) { 61 | return { 62 | code: 1, 63 | msg: '已存在该文集类型' 64 | } 65 | } 66 | let data = await articleTypes.update(query); 67 | return { 68 | code: 0, 69 | data: data 70 | } 71 | } 72 | 73 | async del(id) { 74 | const articleTypes = await this.ctx.model.ArticleTypes.findById(id); 75 | if (!articleTypes) { 76 | return { 77 | code: 404, 78 | msg: '不存在文集类型' 79 | } 80 | } 81 | let data = await articleTypes.destroy(); 82 | return { 83 | code: 0, 84 | data: data.dataValues 85 | } 86 | } 87 | } 88 | 89 | module.exports = ArticleType; 90 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller } = app; 8 | // 后台授权中间件 9 | const auth = app.middleware.auth() 10 | // 页面跳转 11 | router.get('/', controller.page.index); 12 | // 获取验证码 13 | router.get('/getCaptcha', controller.client.login.getCaptcha); 14 | router.get('/admin/getCaptcha', controller.admin.login.getCaptcha); 15 | // 登录 16 | router.post('/login', controller.client.login.login); 17 | // 获取七牛token 18 | router.get('/getQiniuToken', auth, controller.article.getQiniuToken); 19 | 20 | // 通过用户Id获取文集类型 21 | router.get('/getListByUserId', auth, controller.articleType.getListByUserId); 22 | // 通过用户Id获取文集类型 23 | router.post('/addType', auth, controller.articleType.create); 24 | // 通过Id修改文集类型 25 | router.post('/updateType', auth, controller.articleType.update); 26 | // 通过Id删除文集类型 27 | router.post('/delType', auth, controller.articleType.del); 28 | 29 | // 获取文章列表 30 | router.get('/getArticleList', controller.article.getArticleList); 31 | // 通过type_id 获取文章列表 32 | router.get('/getArticleListByTypes', controller.article.getArticleListByTypes); 33 | // 通过type_id 获取文章列表+备份 34 | router.get('/getArticleBackByTypeId', controller.article.getArticleBackByTypeId); 35 | // 通过backId 获取备份表 36 | router.get('/getArticleBackById', controller.article.getArticleBackById); 37 | // 通过id 获取文章列表 38 | router.get('/getArticleById', controller.article.getArticleById); 39 | // 通过id 获取文章详情 40 | router.get('/getArticleDetailById', controller.article.getArticleDetailById); 41 | // 通过type_id 添加文章 42 | router.post('/addArticleByTypeId', auth, controller.article.addArticleByTypeId); 43 | // 修改文章状态 44 | router.post('/updateStatus', auth, controller.article.updateStatus); 45 | // 修改文章 46 | router.post('/updateArticle', auth, controller.article.update); 47 | // 更新修改文章 48 | router.post('/editArticle', auth, controller.article.backEdit); 49 | 50 | // 获取评论列表 51 | router.get('/getCommentListByArticleId', controller.article.getCommentListByArticleId); 52 | // 添加评论 53 | router.post('/addComment', auth, controller.article.addComment); 54 | // 添加评论回复 55 | router.post('/addCommentReply', auth, controller.article.addCommentReply); 56 | // 评论点赞 57 | router.post('/addCommentLoveNum', auth, controller.article.addCommentLoveNum); 58 | 59 | router.get('/users', controller.users.index); 60 | router.get('/test', controller.users.test); 61 | 62 | }; 63 | -------------------------------------------------------------------------------- /database/create.md: -------------------------------------------------------------------------------- 1 | ## MAC mysql 安装 2 | ``` 3 | brew install mysql 4 | brew service start mysql 5 | ``` 6 | 7 | ## 初始化项目 8 | ``` 9 | // 初始化 10 | 11 | egg-init --type=simple --dir=sequelize-project 12 | cd sequelize-project 13 | npm i 14 | 15 | // 安装 16 | 17 | npm install --save egg-sequelize mysql2 18 | 19 | // config/plugin.js 配置 20 | 21 | exports.sequelize = { 22 | enable: true, 23 | package: 'egg-sequelize', 24 | }; 25 | 26 | // config/config.default.js配置 27 | 28 | config.sequelize = { 29 | dialect: 'mysql', 30 | host: '127.0.0.1', 31 | port: 3306, 32 | database: 'egg-sequelize-doc-default', 33 | username: 'root', 34 | password: 'root' 35 | }; 36 | ``` 37 | 38 | ## mysql 创建表 39 | ``` 40 | mysql -u root -e 'CREATE DATABASE IF NOT EXISTS `egg-sequelize-doc-default`;' 41 | ``` 42 | ## 安装 sequelize-cli 43 | ``` 44 | cnpm i sequelize-cli -D 45 | ``` 46 | ## 项目根目录创建 `.sequelizerc`文件 47 | 48 | ``` 49 | 'use strict'; 50 | 51 | const path = require('path'); 52 | 53 | module.exports = { 54 | config: path.join(__dirname, 'database/config.json'), 55 | 'migrations-path': path.join(__dirname, 'database/migrations'), 56 | 'seeders-path': path.join(__dirname, 'database/seeders'), 57 | 'models-path': path.join(__dirname, 'app/model'), 58 | }; 59 | ``` 60 | 61 | ## 初始化 Migrations 配置文件和目录 62 | 63 | ``` 64 | npx sequelize init:config 65 | npx sequelize init:migrations 66 | ``` 67 | 68 | ## 在database/migrations创建表文件 69 | ``` 70 | npx sequelize migration:generate --name=init-users(users表名字) 71 | ``` 72 | 73 | ## 修改init-users文件 74 | ``` 75 | 'use strict'; 76 | 77 | module.exports = { 78 | // 在执行数据库升级时调用的函数,创建 users 表 79 | up: async (queryInterface, Sequelize) => { 80 | const { INTEGER, DATE, STRING } = Sequelize; 81 | await queryInterface.createTable('users', { 82 | id: { type: INTEGER, primaryKey: true, autoIncrement: true }, 83 | name: STRING(30), 84 | age: INTEGER, 85 | created_at: DATE, 86 | updated_at: DATE, 87 | }); 88 | }, 89 | // 在执行数据库降级时调用的函数,删除 users 表 90 | down: async queryInterface => { 91 | await queryInterface.dropTable('users'); 92 | }, 93 | }; 94 | ``` 95 | 96 | ## 执行 migrate 进行数据库变更 97 | ``` 98 | # 升级数据库 99 | npx sequelize db:migrate 100 | # 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更 101 | # npx sequelize db:migrate:undo 102 | # 可以通过 `db:migrate:undo:all` 回退到初始状态 103 | # npx sequelize db:migrate:undo:all 104 | ``` 105 | 106 | [官方文档链接](https://eggjs.org/zh-cn/tutorials/sequelize.html) 107 | -------------------------------------------------------------------------------- /app/service/comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Service = require('egg').Service; 4 | 5 | class Comment extends Service { 6 | // 通过文章id获取评论列表 7 | async getCommentList({ article_id, user_id, offset = 0, limit = 10, order = 'desc' }) { 8 | const { ctx } = this 9 | const options = { 10 | offset, 11 | limit, 12 | order: [[ 'created_at', order ], [ 'id', order ]], 13 | where: { article_id }, 14 | include: [{ 15 | order: [[ 'created_at', 'asc' ], [ 'id', 'asc' ]], 16 | model: this.ctx.model.CommentReply 17 | }] 18 | }; 19 | let data = await ctx.model.Comment.findAndCountAll(options) 20 | let rows = [] 21 | for (let info of data.rows) { 22 | let lovesId = await ctx.model.CommentLove.findOne({ attributes: [ 'id' ], where: { user_id: user_id, comment_id: info.id } }) 23 | info.dataValues.loveId = lovesId 24 | rows.push(info) 25 | } 26 | return { 27 | rows: rows 28 | } 29 | } 30 | // 添加评论 31 | async create(query) { 32 | const { ctx } = this; 33 | let user = await ctx.model.Users.findById(query.user_id); 34 | if (!user) { 35 | return { 36 | code: 401, 37 | msg: '用户不存在' 38 | } 39 | } 40 | query.user_name = user.username 41 | query.user_header_url = user.header_url 42 | query.love_num = 0 43 | query.created_at = new Date() 44 | query.updated_at = new Date() 45 | let data = await ctx.model.Comment.create(query); 46 | if (!data) { 47 | return { 48 | code: 401, 49 | msg: '评论失败' 50 | } 51 | } 52 | await ctx.service.article.addCommentNum(data.article_id) 53 | return { 54 | code: 0, 55 | data: data.dataValues 56 | } 57 | } 58 | // 添加评论回复 59 | async createReply(query) { 60 | const { ctx } = this; 61 | let user = await ctx.model.Users.findById(query.from_user_id); 62 | if (!user) { 63 | return { 64 | code: 401, 65 | msg: '用户不存在' 66 | } 67 | } 68 | query.from_user_name = user.username 69 | query.created_at = new Date() 70 | query.updated_at = new Date() 71 | let data = await ctx.model.CommentReply.create(query); 72 | if (!data) { 73 | return { 74 | code: 1, 75 | msg: '回复失败' 76 | } 77 | } 78 | return { 79 | code: 0, 80 | data: data.dataValues 81 | } 82 | } 83 | // 添加或者取消评论点赞 84 | async LoveNum(id, loveId) { 85 | let num = 0 86 | let option = { 87 | where: { 88 | user_id: this.ctx.userId, 89 | comment_id: id, 90 | } 91 | } 92 | if (loveId) { 93 | option.where.id = loveId 94 | } 95 | let Loves = await this.ctx.model.CommentLove.findOne(option); 96 | if (Loves) { 97 | await Loves.destroy(); 98 | num = -1 99 | } else { 100 | let op = { 101 | comment_id: id, 102 | user_id: this.ctx.userId, 103 | created_at: new Date() 104 | } 105 | Loves = await this.ctx.model.CommentLove.create(op) 106 | num = 1 107 | } 108 | await this.app.model.query('update t_comments set love_num = love_num+:num where id=:id', { type: 'UPDATE', replacements: { id: id, num: num } }).then(results => results); 109 | Loves.dataValues.num = num 110 | return Loves 111 | } 112 | } 113 | 114 | module.exports = Comment; 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 仿简书后端接口平台 2 | 3 | 这个是一个基于egg模仿简书的后端接口,主要是为了学习使用,目前还在迭代开发中 4 | 5 | ## 目前规划 6 | 7 | - 后端 egg.js (已完成部分功能)[源码链接](https://github.com/jieyuanfei/jianshu-egg-api) 8 | - 后台管理 (计划中)[源码链接](https://github.com/jieyuanfei/jianshu-egg-api) 9 | - pc前端 vue (已完成部分功能)[源码链接](https://github.com/jieyuanfei/jianshu-vue-pc) 10 | - 移动端APP react native (已完成部分功能)[源码链接](https://github.com/jieyuanfei/jianshu-react-mobile) 11 | - 小程序 mp-vue (计划中) 12 | 13 | ## 快速入门 14 | 15 | ### 技术栈 16 | 17 | - 后端: [egg.js](https://eggjs.org/zh-cn/)、[mysql](http://www.runoob.com/mysql/mysql-tutorial.html) 18 | 19 | - 移动端APP: [react native](https://reactnative.cn/) 20 | 21 | - 小程序: [mp-vue](https://reactnative.cn/) 22 | 23 | - 前端: 24 | - PC端:[vue](https://cn.vuejs.org/)、[element](http://element-cn.eleme.io/#/zh-CN) 25 | - 后台管理端:[vue](https://cn.vuejs.org/)、[element](http://element-cn.eleme.io/#/zh-CN) 26 | 27 | ### 功能特性 28 | 29 | - 轻量级Markdown编辑器,图片上传七牛 30 | - 基本仿照简书所有的功能 31 | - 部分功能自己添加的(每周一篇博客提交日历提醒功能) 32 | - 标签云 33 | ### 线上地址 34 | [PC端页面展示](http://39.108.125.74/jianshu/#/) 35 | ### 图片演示 36 | #### PC前台 [源码链接](https://github.com/jieyuanfei/jianshu-vue-pc) 37 | 38 | - 首页 39 | ![首页](./screenshot/home.gif) 40 | ![首页2](./screenshot/home2.gif) 41 | 42 | - 文章详情页-评论回复-点赞-收藏 43 | ![详情](./screenshot/detail.gif) 44 | 45 | - 编译文章 46 | ![编译文章](./screenshot/write.gif) 47 | 48 | - 关注 49 | ![关注](./screenshot/follow.gif) 50 | 51 | - 登录注册 52 | ![登录注册](./screenshot/sign.gif) 53 | 54 | - APP下载页面 55 | ![APP下载页面](./screenshot/introduce.gif) 56 | ### 目录结构 57 | 58 | ``` 59 | │ .autod.conf.js 60 | │ .eslintignore 61 | │ .eslintrc 62 | │ .gitignore 63 | │ .travis.yml 64 | │ app.js // 项目启动前执行,比如写入管理员 65 | │ appveyor.yml 66 | │ package.json 67 | │ README.md 68 | │ 69 | ├─db 70 | │ │ db.md // db设计文档 71 | ├─screenshot // 演示图片路径 72 | ├─app 73 | │ │ router.js // 服务端路由 74 | │ │ 75 | │ ├─controller 76 | │ │ 77 | │ ├─extend 78 | │ │ helper.js 79 | │ │ 80 | │ ├─middleware 81 | │ │ auth.js // 登录验证中间件 82 | │ │ 83 | │ ├─model 84 | │ │ 85 | │ │ 86 | │ ├─public 87 | │ │ 88 | │ └─service // service部分用来执行具体的操作 89 | │ 90 | │ 91 | ├─config 92 | │ config.default.js // 项目配置相关 93 | │ plugin.js 94 | │ 95 | └─test // 测试相关 96 | └─app 97 | └─controller 98 | home.test.js 99 | ``` 100 | 101 | ### 全局配置 102 | 103 | ```javascript 104 | module.exports = appInfo => { 105 | return { 106 | keys: appInfo.name + '_153332185447_3632', 107 | cluster: { 108 | listen: { // 启动监听的端口号配置 109 | path: '', 110 | port: 7002, 111 | hostname: '127.0.0.1', 112 | } 113 | }, 114 | sequelize: { 115 | dialect: 'mysql', // support: mysql, mariadb, postgres, mssql 116 | database: '数据库名字', 117 | host: 'ip', 118 | username: '用户名', 119 | password: '密码', 120 | port: 3306, 121 | }, 122 | user: { // 初始化管理员的账号 123 | userName: 'admin', 124 | password: 'admin', 125 | }, 126 | session: { 127 | maxAge: 3600 * 1000, 128 | }, 129 | jwt: { 130 | cert: 'huanggegehaoshuai' // jwt秘钥 131 | }, 132 | qiniu: { // 这里填写你七牛的Access Key和Secret Key 133 | ak: '', 134 | sk: '' 135 | } 136 | } 137 | }; 138 | ``` 139 | 140 | 141 | ### 本地运行 142 | 143 | 安装mysql数据库和node环境。 144 | 145 | ``` bash 146 | # 安装服务端依赖 147 | npm install 148 | # 运行服务 149 | npm run dev 150 | 151 | ``` 152 | 153 | ### 打包 154 | 155 | ```bash 156 | # 在前台和后台目录分别 157 | npm run build 158 | # 在项目根目录 159 | npm install --production 160 | # 启动 161 | npm start 162 | ``` 163 | -------------------------------------------------------------------------------- /db/db.md: -------------------------------------------------------------------------------- 1 | ## 简书数据库设计 2 | 3 | ### 用户基础表 t_user_base 4 | 5 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 6 | | :------: |:------:|:------: | :------: | :------: | 7 | | id | varchar | 30 | 表主键id | - | 8 | | header_url | varchar | 150 | 用户头像 | - | 9 | | user_name | varchar | 30 | 用户名称 | - | 10 | | user_email | varchar | 30 | 电子邮件 | - | 11 | | phone | varchar | 11 | 用户手机 | - | 12 | | phone_validate | int | 1 | 用户手机是否已经校验 | 0 已校验 / 1 未校验 | 13 | | edit_type | int | 1 | 编译文本类型 | 0 富文本 / 1 markdown | 14 | | language | int | 1 | 语言 | 0 简体中文 / 1 繁体 / 2 英文 | 15 | | receive_msg | int | 1 | 接收简讯 | 0 所有 / 1 我关注的 | 16 | | notice | int | 1 | 邮件通知 | 0 所有动态 / 1 每天未读汇总 / 2 不接收 | 17 | | create_time | date | 100 | 创建时间 | - | 18 | 19 | ### 个人资料表 t_user_data 20 | 21 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 22 | | :------: |:------:|:------: | :------: | :------: | 23 | | id | varchar | 30 | 表主键id | - | 24 | | user_id | varchar | 30 | 用户id | - | 25 | | sex | int | 1 | 性别 | 0 男 / 1 女 / 2 保密 | 26 | | introduction | varchar | 150 | 个人简介 | - | 27 | | web | varchar | 500 | 个人网站链接 | - | 28 | | wx_qrcode_url | varchar | 150 | 微信二维码 | - | 29 | | create_time | date | 100 | 创建时间 | - | 30 | 31 | ### 黑名单表 t_user_blacklist 32 | 33 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 34 | | :------: |:------:|:------: | :------: | :------: | 35 | | id | varchar | 30 | 表主键id | - | 36 | | user_id | varchar | 30 | 用户id | - | 37 | | black_user_id | varchar | 30 | 拉黑用户id | - | 38 | | create_time | date | 100 | 创建时间 | - | 39 | 40 | ### 赞赏表 t_user_appreciate 41 | 42 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 43 | | :------: |:------:|:------: | :------: | :------: | 44 | | id | varchar | 30 | 表主键id | - | 45 | | user_id | varchar | 30 | 用户id | - | 46 | | describe | varchar | 300 | 描述 | - | 47 | | status | int | 1 | 开启状态 | 0 关闭 / 1 开启 | 48 | | create_time | date | 100 | 创建时间 | - | 49 | 50 | ### 文章类型表/标签表 t_article_type 51 | 52 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 53 | | :------: |:------:|:------: | :------: | :------: | 54 | | id | varchar | 30 | 表主键id | - | 55 | | type_name | varchar | 30 | 类型名称 | - | 56 | | user_id | varchar | 30 | 用户id | - | 57 | | delete | int | 1 | 是否删除 | 0 正常/1 删除 | 58 | | create_time | date | 100 | 创建时间 | - | 59 | 60 | ### 文章表 t_article 61 | 62 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 63 | | :------: |:------:|:------: | :------: | :------: | 64 | | id | varchar | 30 | 表主键id | - | 65 | | title | varchar | 100 | 标题 | - | 66 | | content | varchar | 10000 | 内容 | - | 67 | | user_id | varchar | 30 | 用户id | - | 68 | | type_id | varchar | 30 | 类型id | - | 69 | | article_num | int | 100 | 文章字数 | - | 70 | | ready_num | int | 100 | 阅读数 | - | 71 | | like_num | int | 100 | 喜欢 | - | 72 | | comment_num | int | 100 | 评论数 | - | 73 | | edit_time | date | 100 | 文章编译时间 | - | 74 | | status | int | 1 | 文章状态 | 0 已发布/ 1 删除 /2 私密 | 75 | 76 | ### 文章备份表/草稿表/历史表 t_article_backup 77 | 78 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 79 | | :------: |:------:|:------: | :------: | :------: | 80 | | id | varchar | 30 | 表主键id | - | 81 | | article_id | varchar | 30 | 文章表id | - | 82 | | title | varchar | 100 | 标题 | - | 83 | | content | varchar | 10000 | 内容 | - | 84 | | user_id | varchar | 30 | 用户id | - | 85 | | type | int | 1 | 表类型 | 0 草稿 / 1 历史表 | 86 | | delete | int | 1 | 文章是否删除 | 0 正常 / 1 删除 | 87 | | edit_time | date | 100 | 文章编译时间 | - | 88 | 89 | ### 评论表 t_comment 90 | 91 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 92 | | :------: |:------:|:------: | :------: | :------: | 93 | | id | varchar | 30 | 表主键id | - | 94 | | article_id | varchar | 255 | 文章表id | - | 95 | | content | varchar | 10000 | 内容 | - | 96 | | user_id | varchar | 30 | 用户id | - | 97 | | edit_time | date | 100 | 编译时间 | - | 98 | 99 | ### 评论子表 t_comment_multi 100 | 101 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 102 | | :------: |:------:|:------: | :------: | :------: | 103 | | id | varchar | 30 | 表主键id | - | 104 | | comment_id | varchar | 30 | 文章表id | - | 105 | | content | varchar | 10000 | 内容 | - | 106 | | user_id | varchar | 30 | 用户id | - | 107 | | edit_time | date | 100 | 编译时间 | - | 108 | 109 | ### 收藏/喜欢表 t_collection 110 | 111 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 112 | | :------: |:------:|:------: | :------: | :------: | 113 | | id | varchar | 30 | 表主键id | - | 114 | | article_id | varchar | 250 | 文章id | - | 115 | | article_url | varchar | 250 | 文章链接 | - | 116 | | user_id | varchar | 30 | 用户id | - | 117 | | type | int | 1 | 收藏类型 | 0 收藏 / 1 喜欢 | 118 | | create_time | date | 100 | 编译时间 | - | 119 | 120 | ### 关注表 t_follow 121 | 122 | | 字段 | 类型 | 字段长度 | 说明 | 备注 | 123 | | :------: |:------:|:------: | :------: | :------: | 124 | | id | varchar | 30 | 表主键id | - | 125 | | author_id | varchar | 250 | 作者id | - | 126 | | user_id | varchar | 30 | 用户id | - | 127 | | create_time | date | 100 | 编译时间 | - | 128 | -------------------------------------------------------------------------------- /app/controller/article.js: -------------------------------------------------------------------------------- 1 | const Controller = require('egg').Controller 2 | const cheerio = require('cheerio'); 3 | 4 | class ArticleController extends Controller { 5 | // 获取七牛token 6 | async getQiniuToken() { 7 | const { ctx } = this 8 | let resMsg = { 9 | code: 0, 10 | data: {}, 11 | msg: '获取token成功' 12 | } 13 | let uploadToken = await ctx.service.article.getQiniuToken() 14 | console.log(uploadToken) 15 | resMsg.data.token = uploadToken 16 | ctx.success(resMsg) 17 | } 18 | // 获取所有文章列表 19 | async getArticleList() { 20 | const { ctx } = this 21 | const query = { 22 | offset: ctx.helper.parseInt(ctx.query.offset || 0), 23 | limit: ctx.helper.parseInt(ctx.query.limit || 10), 24 | }; 25 | let data = await ctx.service.article.getArticleList(query) 26 | ctx.success(data) 27 | } 28 | 29 | // 根据类型id获取文章列表 30 | async getArticleListByTypes() { 31 | const { ctx } = this 32 | let type_id = ctx.query.type_id 33 | let wheres = { 34 | type_id 35 | } 36 | let data = await ctx.service.article.getArticleListByOther(wheres) 37 | ctx.success(data) 38 | } 39 | 40 | // 根据Id获取文章 41 | async getArticleById() { 42 | const { ctx } = this 43 | let id = ctx.query.id 44 | if (!id) { 45 | ctx.error(404, '文章id不能为空') 46 | } 47 | let data = await ctx.service.article.getArticleById(id) 48 | ctx.success(data) 49 | } 50 | 51 | // 根据文章id 获取文章详情 52 | async getArticleDetailById() { 53 | const { ctx } = this 54 | let id = ctx.query.id 55 | if (!id) { 56 | ctx.error(404, '文章id不能为空') 57 | } 58 | let data = await ctx.service.article.getArticleById(id) 59 | let user = await ctx.service.user.find(data.data.user_id) 60 | let users = { 61 | id: user.data.id, 62 | username: user.data.username, 63 | header_url: user.data.header_url 64 | }; 65 | 66 | ctx.success({ article: data.data, user: users }) 67 | } 68 | 69 | // 根据typeId创建文章 70 | async addArticleByTypeId() { 71 | const { ctx } = this 72 | const { type_id } = ctx.request.body 73 | let query = { 74 | type_id, 75 | user_id: ctx.userId 76 | } 77 | let data = await ctx.service.article.create(query) 78 | ctx.success(data) 79 | } 80 | 81 | // 真实删除文章 82 | async del() { 83 | const ctx = this.ctx; 84 | const { id } = ctx.request.body 85 | const article = await ctx.service.article.del(id); 86 | if (article.code === 0) { 87 | ctx.success(article.data) 88 | } else { 89 | ctx.error(article.code, article.msg) 90 | } 91 | } 92 | 93 | // 修改状态,删除文章 94 | async updateStatus() { 95 | const ctx = this.ctx; 96 | const { id, status, backId } = ctx.request.body 97 | let query = { 98 | id, 99 | backId, 100 | status 101 | } 102 | const article = await ctx.service.article.update(query); 103 | if (article.code === 0) { 104 | ctx.success(article.data) 105 | } else { 106 | ctx.error(article.code, article.msg) 107 | } 108 | } 109 | 110 | // 更新文章 111 | async update() { 112 | const ctx = this.ctx; 113 | const { query, html } = ctx.request.body 114 | let $ = cheerio.load('
' + html + '
'); 115 | let text = $('.html').text().replace(/\s+/g, ''); 116 | query.text = text; 117 | query.article_num = text.length; 118 | if (text.length > 254) { // 描述文字长度控制在255个字就ok 119 | query.text = text.substring(0, 254) 120 | } 121 | let images = $('.html').find('img'); 122 | let imgList = []; 123 | // 保存3张图片就够了 124 | images.each(function(index) { 125 | if (index > 2) { 126 | return false; 127 | } 128 | imgList.push($(this).attr('src')) 129 | }) 130 | query.images = JSON.stringify(imgList) 131 | const article = await ctx.service.article.update(query); 132 | if (article.code === 0) { 133 | ctx.success(article.data) 134 | } else { 135 | ctx.error(article.code, article.msg) 136 | } 137 | } 138 | 139 | // 修改发布状态 140 | async backEdit() { 141 | const ctx = this.ctx; 142 | const { query, html } = ctx.request.body 143 | let $ = cheerio.load('
' + html + '
'); 144 | let text = $('.html').text().replace(/\s+/g, ''); 145 | query.text = text; 146 | query.article_num = text.length; 147 | if (text.length > 254) { // 描述文字长度控制在255个字就ok 148 | query.text = text.substring(0, 254) 149 | } 150 | let images = $('.html').find('img'); 151 | let imgList = []; 152 | // 保存3张图片就够了 153 | images.each(function(index) { 154 | if (index > 2) { 155 | return false; 156 | } 157 | imgList.push($(this).attr('src')) 158 | }) 159 | query.images = JSON.stringify(imgList) 160 | query.text = text; 161 | query.article_num = text.length 162 | let article = await ctx.service.article.createBack(query); 163 | 164 | if (article.code === 0) { 165 | ctx.success(article.data) 166 | } else { 167 | ctx.error(article.code, article.msg) 168 | } 169 | } 170 | 171 | // 通过类型Id 获取文章列表和备份表数据 172 | async getArticleBackByTypeId() { 173 | const { ctx } = this 174 | let type_id = ctx.query.type_id 175 | let data = await ctx.service.article.getArticleBackByTypeId(type_id) 176 | ctx.success(data) 177 | } 178 | // 根据backId获取文章 179 | async getArticleBackById() { 180 | const { ctx } = this 181 | let id = ctx.query.id 182 | if (!id) { 183 | ctx.error(404, '文章id不能为空') 184 | } 185 | let data = await ctx.service.article.getArticleBackById(id) 186 | ctx.success(data) 187 | } 188 | // 获取评论 189 | async getCommentListByArticleId() { 190 | const { ctx } = this 191 | const query = { 192 | article_id: ctx.query.article_id, 193 | user_id: ctx.query.user_id, 194 | offset: ctx.helper.parseInt(ctx.query.offset || 0), 195 | limit: ctx.helper.parseInt(ctx.query.limit || 10), 196 | }; 197 | let data = await ctx.service.comment.getCommentList(query); 198 | ctx.success(data); 199 | } 200 | // 添加评论 201 | async addComment() { 202 | const { ctx } = this 203 | const { query } = ctx.request.body 204 | let user_id = ctx.userId 205 | if (user_id) { 206 | ctx.error(401, '请先登录') 207 | } 208 | query.user_id = user_id 209 | let data = await ctx.service.comment.create(query) 210 | if (data.code === 0) { 211 | ctx.success(data) 212 | } else { 213 | ctx.error(data) 214 | } 215 | } 216 | // 添加评论回复 217 | async addCommentReply() { 218 | const { ctx } = this 219 | const { query } = ctx.request.body 220 | let user_id = ctx.userId 221 | if (user_id) { 222 | ctx.error(401, '请先登录') 223 | } 224 | query.from_user_id = user_id 225 | let data = await ctx.service.comment.createReply(query) 226 | ctx.success(data) 227 | } 228 | // 评论点赞 229 | async addCommentLoveNum() { 230 | const { ctx } = this 231 | const { id, LoveId } = ctx.request.body 232 | let data = await ctx.service.comment.LoveNum(id, LoveId) 233 | ctx.success(data) 234 | } 235 | } 236 | 237 | module.exports = ArticleController 238 | -------------------------------------------------------------------------------- /app/service/article.js: -------------------------------------------------------------------------------- 1 | const Service = require('egg').Service; 2 | const qiniu = require('qiniu') 3 | class ArticleService extends Service { 4 | // 根据用户id获取文章列表 5 | async getArticleList({ offset = 0, limit = 10, user_id }) { 6 | const options = { 7 | offset, 8 | limit, 9 | attributes: [ 'id', 'title', 'text', 'images', 'user_id', 'type_id', 'article_num', 'ready_num', 'like_num', 'comment_num', 'status', 'created_at', 'updated_at' ], 10 | order: [[ 'created_at', 'desc' ], [ 'id', 'desc' ]], 11 | include: { 12 | attributes: [ 'username', 'header_url' ], 13 | model: this.ctx.model.Users 14 | } 15 | }; 16 | options.where = { 17 | status: 0 18 | } 19 | if (user_id) { 20 | options.where = { 21 | user_id, 22 | }; 23 | } 24 | let data = await this.ctx.model.Article.findAndCountAll(options) 25 | data.rows = data.rows.map(info => { 26 | if (info.images) { 27 | info.images = JSON.parse(info.images) 28 | } else { 29 | info.images = []; 30 | } 31 | return info 32 | }) 33 | return data; 34 | } 35 | // 根据其他条件添加 36 | async getArticleListByOther(wheres, offset, limit) { 37 | const options = { 38 | offset, 39 | limit, 40 | // attributes: [ 'id', 'title', 'content', 'user_id', 'created_at', 'updated_at' ], 41 | order: [[ 'created_at', 'desc' ], [ 'id', 'desc' ]], 42 | }; 43 | if (offset) { 44 | options.offset = offset 45 | } 46 | if (limit) { 47 | options.limit = limit 48 | } 49 | if (wheres) { 50 | options.where = wheres 51 | options.where.status = [ 0, 1, 2 ] 52 | } else { 53 | options.where = { 54 | status: [ 0, 1, 2 ] 55 | } 56 | } 57 | return this.ctx.model.Article.findAndCountAll(options); 58 | } 59 | // 根据Id 60 | async getArticleById(id) { 61 | let data = await this.ctx.model.Article.findById(id); 62 | if (!data) { 63 | return { 64 | code: 404, 65 | msg: '不存在文集类型' 66 | } 67 | } 68 | data.ready_num = data.ready_num + 1; 69 | this.addReadyNum(data.id) 70 | return { 71 | code: 0, 72 | data: data 73 | } 74 | } 75 | // 阅读数加 1 76 | async addReadyNum(id) { 77 | let data = this.app.model.query('update t_articles set ready_num = ready_num+1 where id=:id', { type: 'UPDATE', replacements: { id: id } }).then(results => results); 78 | return data 79 | } 80 | // 评论数加 1 81 | async addCommentNum(id) { 82 | let data = this.app.model.query('update t_articles set comment_num = comment_num+1 where id=:id', { type: 'UPDATE', replacements: { id: id } }).then(results => results); 83 | return data 84 | } 85 | // 新增 86 | async create(query) { 87 | const { ctx } = this; 88 | // 使用事务管理 89 | try { 90 | let option = { 91 | title: '标题栏', 92 | content: ' ', 93 | user_id: query.user_id, 94 | type_id: query.type_id, 95 | article_num: 0, 96 | ready_num: 0, 97 | like_num: 0, 98 | comment_num: 0, 99 | status: 1, 100 | created_at: new Date(), 101 | updated_at: new Date(), 102 | } 103 | let data = await ctx.model.Article.create(option); 104 | let optionBack = { 105 | article_id: data.id, 106 | type_id: data.type_id, 107 | title: data.title, 108 | text: '', 109 | content: '', 110 | article_num: 0, 111 | status: 0, 112 | created_at: new Date(), 113 | updated_at: new Date(), 114 | } 115 | let articleBack = await ctx.model.ArticleBack.create(optionBack); 116 | return { 117 | code: 0, 118 | data: { 119 | id: data.id, 120 | status: data.status, 121 | backId: articleBack.id, 122 | title: data.title, 123 | text: '', 124 | article_num: 0, 125 | } 126 | } 127 | } catch (e) { 128 | 129 | return { 130 | code: 1, 131 | msg: '插入失败' 132 | } 133 | } 134 | } 135 | async del(id) { 136 | const article = await this.ctx.model.Article.findById(id); 137 | if (!article) { 138 | return { 139 | code: 404, 140 | msg: '不存在文集类型' 141 | } 142 | } 143 | let data = await article.destroy(); 144 | return { 145 | code: 0, 146 | data: data.dataValues 147 | } 148 | } 149 | async update(query) { 150 | const article = await this.ctx.model.Article.findById(query.id); 151 | if (!article) { 152 | return { 153 | code: 404, 154 | msg: '文章已被删除' 155 | } 156 | } 157 | let option = {} 158 | if (query.id) { 159 | option.id = query.id 160 | } 161 | option.status = query.status 162 | if (query.status === 0) { // 0 发布 重备份表获取title,content..... 163 | let backup = await this.ctx.model.ArticleBack.findById(query.backId) 164 | if (backup) { 165 | option.title = backup.title 166 | option.text = backup.text 167 | option.images = backup.images 168 | option.content = backup.content 169 | option.article_num = backup.article_num 170 | option.updated_at = new Date() 171 | } 172 | } 173 | let data = await article.update(option); 174 | return { 175 | code: 0, 176 | data: data 177 | } 178 | } 179 | // 生成七牛token 180 | async getQiniuToken() { 181 | const { app } = this 182 | // 这里需要七牛的Access Key和Secret Key 183 | let mac = new qiniu.auth.digest.Mac(app.config.qiniu.ak, app.config.qiniu.sk); 184 | let options = { 185 | scope: 'jianshu', 186 | }; 187 | let putPolicy = new qiniu.rs.PutPolicy(options); 188 | let uploadToken = putPolicy.uploadToken(mac); 189 | return uploadToken 190 | } 191 | 192 | // 备份表数据更新 193 | async createBack(query) { 194 | const { ctx } = this; 195 | let option = { 196 | article_id: query.article_id, 197 | type_id: query.type_id, 198 | title: query.title, 199 | images: query.images, 200 | content: query.content, 201 | text: query.text, 202 | article_num: query.article_num, 203 | status: 0, 204 | created_at: new Date(), 205 | updated_at: new Date(), 206 | } 207 | let data = await ctx.model.ArticleBack.create(option); 208 | if (!data) { 209 | return { 210 | code: 1, 211 | msg: '插入失败' 212 | } 213 | } 214 | return { 215 | code: 0, 216 | data: data.dataValues 217 | } 218 | 219 | } 220 | // 备份表更新 221 | async updateBack(query) { 222 | const articleBack = await this.ctx.model.ArticleBack.findById(query.id); 223 | if (!articleBack) { 224 | return { 225 | code: 404, 226 | msg: '文章已被删除' 227 | } 228 | } 229 | let data = await articleBack.update(query); 230 | return { 231 | code: 0, 232 | data: data 233 | } 234 | } 235 | // 根据类型ID 获取文章列表 236 | async getArticleBackByTypeId(typeId) { 237 | const options = { 238 | attributes: [ 'id', 'status' ], 239 | where: { type_id: typeId, status: [ 0, 1, 2 ] }, 240 | order: [[ 'updated_at', 'desc' ], [ 'id', 'desc' ]] 241 | }; 242 | let articles = await this.ctx.model.Article.findAndCountAll(options); 243 | let rows = [] 244 | for (let info of articles.rows) { 245 | let op = { 246 | where: { article_id: info.id }, 247 | order: [[ 'updated_at', 'desc' ]], 248 | limit: 1 249 | } 250 | let articleBack = await this.ctx.model.ArticleBack.findAndCountAll(op) 251 | rows.push({ 252 | id: info.id, 253 | status: info.status, 254 | backId: articleBack.rows[0].id, 255 | title: articleBack.rows[0].title, 256 | text: articleBack.rows[0].text, 257 | article_num: articleBack.rows[0].title, 258 | }) 259 | } 260 | articles.rows = rows; 261 | return articles 262 | } 263 | // 根据backId 获取备份表信息 264 | async getArticleBackById(id) { 265 | let data = await this.ctx.model.ArticleBack.findById(id); 266 | if (!data) { 267 | return { 268 | code: 404, 269 | msg: '不存在文集类型' 270 | } 271 | } 272 | return { 273 | code: 0, 274 | data: data 275 | } 276 | } 277 | } 278 | 279 | module.exports = ArticleService 280 | --------------------------------------------------------------------------------