├── .eslintignore ├── src ├── cache │ ├── README.md │ ├── works │ │ └── publish.js │ └── index.js ├── models │ ├── README.md │ ├── AdminModel.js │ ├── UserModel.js │ └── WorksModel.js ├── utils │ ├── env.js │ ├── genPassword.js │ ├── cryp.js │ ├── util.js │ └── jwt.js ├── config │ ├── index.js │ ├── constant.js │ └── envs │ │ └── dev.js ├── middlewares │ ├── jwt.js │ ├── cors.js │ ├── loginCheck.js │ └── genValidator.js ├── validator │ ├── users.js │ ├── works.js │ ├── admin.js │ └── template.js ├── db │ ├── seq │ │ ├── types.js │ │ ├── seq.js │ │ └── utils │ │ │ └── sync-alter.js │ ├── redis.js │ └── mysql2.js ├── res-model │ ├── failInfo.js │ └── index.js ├── routes │ ├── index.js │ ├── admin.js │ ├── users.js │ ├── works.js │ └── template.js ├── service │ ├── admin.js │ ├── users.js │ ├── template.js │ └── works.js ├── controller │ ├── admin.js │ ├── users.js │ ├── works.js │ └── template.js └── app.js ├── .vscode └── settings.json ├── .dockerignore ├── __test__ ├── README.md └── demo.test.js ├── ali-node.config.json ├── bin ├── pm2-prd-dev.config.js ├── pm2AppConf.js ├── pm2-prd.config.js ├── create-version-tag.sh ├── deploy.sh └── www ├── docker-compose.yml ├── README.md ├── Dockerfile ├── .eslintrc.js ├── .prettierrc.js ├── .github └── workflows │ ├── test.yml │ ├── deploy-prd.yml │ └── deploy-dev.yml ├── LICENSE ├── .gitignore └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | wfp_test/ -------------------------------------------------------------------------------- /src/cache/README.md: -------------------------------------------------------------------------------- 1 | # 缓存 2 | 3 | 调用 redis 服务 4 | -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # models 2 | 3 | 数据库模型, sequelize 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | logs 4 | .docker-volumes 5 | wfp_test -------------------------------------------------------------------------------- /__test__/README.md: -------------------------------------------------------------------------------- 1 | # 单元测试 2 | 3 | - 测试单元,而不是整体 4 | - 测试逻辑,而不是平铺直叙的代码 5 | - 不要依赖于环境,不要 mock 6 | -------------------------------------------------------------------------------- /ali-node.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appid": "123", 3 | "secret": "xxx", 4 | "logdir": "/tmp/admin-server", 5 | "packages": "~/lego-team/admin-server/package.json" 6 | } -------------------------------------------------------------------------------- /__test__/demo.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description jest demo 3 | * @author 双越 4 | */ 5 | 6 | test('1+1 = 2', () => { 7 | expect(1 + 1).toBe(2) 8 | }) 9 | 10 | test('demo', () => { 11 | expect(1).toBe(1) 12 | }) 13 | -------------------------------------------------------------------------------- /bin/pm2-prd-dev.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pm2 配置文件,dev 测试机 3 | * @author 双越 4 | */ 5 | 6 | const appConf = require('./pm2AppConf') 7 | 8 | // 为了测试方便,pm2 进程设置为 1 9 | appConf.instances = 1 10 | 11 | module.exports = { 12 | apps: [appConf], 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/env.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 环境变量 3 | * @author 双越 4 | */ 5 | 6 | const ENV = process.env.NODE_ENV 7 | 8 | module.exports = { 9 | ENV, 10 | isPrd: ENV === 'production', 11 | isPrdDev: ENV === 'prd_dev', 12 | isDev: ENV === 'dev', 13 | isTest: ENV === 'test', 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | admin-server: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | image: admin-server # 依赖于当前 Dockerfile 创建镜像 8 | container_name: admin-server 9 | ports: 10 | - 8084:3003 # 宿主机通过 8084 访问 11 | -------------------------------------------------------------------------------- /src/utils/genPassword.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 生成一个密码。手机号注册时,需要生成一个随机的密码 3 | * @author 双越 4 | */ 5 | 6 | const { v4: uuidV4 } = require('uuid') 7 | 8 | /** 9 | * 生成一个密码 10 | */ 11 | module.exports = function genPassword() { 12 | const s = uuidV4() // 格式如 5e79b94b-548a-444a-943a-8a09377e3744 13 | return s.split('-')[0] 14 | } 15 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 配置项 3 | * @author 双越 4 | */ 5 | 6 | const { isPrd, isPrdDev } = require('../utils/env') 7 | 8 | // 获取各个环境的不同配置文件 9 | let fileName = 'dev.js' 10 | if (isPrdDev) fileName = 'prd-dev.js' 11 | if (isPrd) fileName = 'prd.js' 12 | 13 | const conf = require(`./envs/${fileName}`) // eslint-disable-line 14 | 15 | module.exports = conf 16 | -------------------------------------------------------------------------------- /src/middlewares/jwt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 封装 jwt 插件 3 | * @author 双越 4 | */ 5 | 6 | const jwtKoa = require('koa-jwt') 7 | const { JWT_SECRET, JWT_IGNORE_PATH } = require('../config/constant') 8 | 9 | module.exports = jwtKoa({ 10 | secret: JWT_SECRET, 11 | cookie: 'jwt_token', // 使用 cookie 存储 token 12 | }).unless({ 13 | // 定义哪些路由忽略 jwt 验证 14 | path: JWT_IGNORE_PATH, 15 | }) 16 | -------------------------------------------------------------------------------- /src/validator/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 数据校验 users 3 | * @author 双越 4 | */ 5 | 6 | const userIdsSchema = { 7 | type: 'object', 8 | required: ['ids'], 9 | properties: { 10 | ids: { 11 | type: 'string', 12 | pattern: '^\\d+(,\\d+)*$', // 格式如 '1' 或 '1,2,3' 13 | }, 14 | }, 15 | } 16 | 17 | module.exports = { 18 | userIdsSchema, 19 | } 20 | -------------------------------------------------------------------------------- /src/validator/works.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 数据校验 works 3 | * @author 双越 4 | */ 5 | 6 | const worksIdsSchema = { 7 | type: 'object', 8 | required: ['ids'], 9 | properties: { 10 | ids: { 11 | type: 'string', 12 | pattern: '^\\d+(,\\d+)*$', // 格式如 '1' 或 '1,2,3' 13 | }, 14 | }, 15 | } 16 | 17 | module.exports = { 18 | worksIdsSchema, 19 | } 20 | -------------------------------------------------------------------------------- /src/config/constant.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 常量配置 3 | * @author 双越 4 | */ 5 | 6 | module.exports = { 7 | // 密码加密 秘钥 8 | PASSWORD_SECRET: 'xxx', 9 | 10 | // jwt 秘钥 11 | JWT_SECRET: 'xxx', 12 | 13 | // jwt 可忽略的 path:全部忽略即可,需要登录验证的,自己用 loginCheck 14 | JWT_IGNORE_PATH: [/\//], 15 | // JWT_IGNORE_PATH: [/^\/admin\/login/, /\//], 16 | 17 | // 查询列表,默认分页配置 18 | DEFAULT_PAGE_SIZE: 10, 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # admin-server 2 | 3 | 后台管理系统 - server 开源代码,欢迎 star ! 4 | 5 | ## 本地运行 6 | 7 | - 安装 `npm i` 8 | - 配置本地数据库,在 `src/config/envs/dev.js` 9 | - 运行 `npm run dev` 10 | 11 | ## 注意事项 12 | 13 | 代码开源之后,屏蔽了一些信息(如线上数据库、第三方服务 secret 、服务器等)。所以以下流程无法顺利执行: 14 | 15 | - 从 0 到 1 的设计过程,commit 记录 16 | - pre commit 检查:单元测试、接口测试 17 | - CI/CD 18 | - 发布到测试机 19 | - 发布上线/回滚 20 | - 运维和监控 21 | 22 | 想了解这些,可去关注我们的《Web 前端架构师》课程,其中都有详细讲解。 23 | -------------------------------------------------------------------------------- /src/db/seq/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 封装 sequelize 类型,参考 https://github.com/demopark/sequelize-docs-Zh-CN/blob/master/data-types.md 3 | * @author 双越 4 | */ 5 | 6 | const Sequelize = require('sequelize') 7 | 8 | module.exports = { 9 | STRING: Sequelize.STRING, // VARCHAR(255) 10 | TEXT: Sequelize.TEXT, // TEXT 11 | INTEGER: Sequelize.INTEGER, 12 | BOOLEAN: Sequelize.BOOLEAN, 13 | DATE: Sequelize.DATE, 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM node:14 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | # 设置时区 7 | RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone 8 | 9 | # 安装 10 | RUN npm set registry https://registry.npm.taobao.org 11 | RUN npm i 12 | 13 | # 宿主机 ip 指向 docker-host ,以方便 docker 内部访问宿主机 14 | CMD /sbin/ip route|awk '/default/ { print $3,"\tdocker-host" }' >> /etc/hosts && npm run prd-dev && npx pm2 log 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | commonjs: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | es6: true, 8 | }, 9 | extends: ['airbnb-base', 'plugin:prettier/recommended'], 10 | parserOptions: { 11 | ecmaVersion: 12, 12 | }, 13 | plugins: ['prettier'], 14 | rules: { 15 | 'no-unused-vars': 0, 16 | 'no-console': 'off', 17 | 'max-classes-per-file': 0, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/db/redis.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 连接 redis 3 | * @author 双越 4 | */ 5 | 6 | const redis = require('redis') 7 | const { redisConf } = require('../config/index') 8 | 9 | // 创建客户端 10 | const { port, host, password } = redisConf 11 | const opt = {} 12 | if (password) { 13 | opt.password = password // prd 环境需要密码 14 | } 15 | const redisClient = redis.createClient(port, host, opt) 16 | redisClient.on('error', err => { 17 | console.error('redis connect error', err) 18 | }) 19 | 20 | module.exports = redisClient 21 | -------------------------------------------------------------------------------- /src/validator/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 数据校验 admin 3 | * @author 双越 4 | */ 5 | 6 | const adminInfoSchema = { 7 | type: 'object', 8 | required: ['username', 'password'], 9 | properties: { 10 | username: { 11 | type: 'string', 12 | pattern: '^\\w+$', // 字母数字下划线 13 | maxLength: 255, 14 | }, 15 | password: { 16 | type: 'string', 17 | maxLength: 255, 18 | }, 19 | }, 20 | } 21 | 22 | module.exports = { 23 | adminInfoSchema, 24 | } 25 | -------------------------------------------------------------------------------- /src/db/mysql2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description mysql2 连接测试(暂时不用,先保留) 3 | * @author 双越 4 | */ 5 | 6 | const mysql = require('mysql2/promise') 7 | const { mysqlConf } = require('../config/index') 8 | 9 | async function testMysqlConn() { 10 | const connection = await mysql.createConnection(mysqlConf) 11 | const [rows] = await connection.execute('select now();') 12 | return rows 13 | } 14 | 15 | // ;(async () => { 16 | // const rows = await testMysqlConn() 17 | // console.log(rows) 18 | // })() 19 | 20 | module.exports = testMysqlConn 21 | -------------------------------------------------------------------------------- /src/cache/works/publish.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 发布作品 缓存 3 | * @author 双越 4 | */ 5 | 6 | const { cacheSet } = require('../index') 7 | 8 | // cache key 前缀,重要!!否则数据容易混乱 9 | // 必须和 biz-editor-server 保持一直,且必须链接一个 redis-server 10 | const PREFIX = 'publishWorkId-' 11 | 12 | /** 13 | * 发布作品,缓存失效 14 | * @param {string} id 作品 id 15 | */ 16 | function publishWorkClearCache(id) { 17 | const key = `${PREFIX}${id}` 18 | cacheSet( 19 | key, 20 | '' // 清空内容 21 | ) 22 | } 23 | 24 | module.exports = { 25 | publishWorkClearCache, 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/cryp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 加密 3 | * @author 双越老师 4 | */ 5 | 6 | const crypto = require('crypto') 7 | const { PASSWORD_SECRET } = require('../config/constant') 8 | 9 | // md5 加密 10 | function md5Fn(content) { 11 | const md5 = crypto.createHash('md5') 12 | return md5.update(content).digest('hex') 13 | } 14 | 15 | /** 16 | * 加密 17 | * @param {string} content 要加密的内容 18 | */ 19 | function doCrypto(content) { 20 | const str = `password=${content}&key=${PASSWORD_SECRET}` 21 | return md5Fn(str) 22 | } 23 | 24 | module.exports = doCrypto 25 | -------------------------------------------------------------------------------- /bin/pm2AppConf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pm2 app 配置信息 3 | */ 4 | 5 | const os = require('os') 6 | 7 | const cpuCoreLength = os.cpus().length // CPU 几核 8 | 9 | module.exports = { 10 | name: 'admin-server', 11 | script: 'bin/www', 12 | // watch: true, 13 | ignore_watch: ['node_modules', '__test__', 'logs'], 14 | instances: cpuCoreLength, 15 | error_file: './logs/err.log', 16 | out_file: './logs/out.log', 17 | log_date_format: 'YYYY-MM-DD HH:mm:ss Z', // Z 表示使用当前时区的时间格式 18 | combine_logs: true, // 多个实例,合并日志 19 | max_memory_restart: '300M', // 内存占用超过 300M ,则重启 20 | } 21 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 箭头函数只有一个参数的时候可以忽略括号 3 | arrowParens: 'avoid', 4 | // 括号内部不要出现空格 5 | bracketSpacing: true, 6 | // 行结束符使用 Unix 格式 7 | endOfLine: 'lf', 8 | // true: Put > on the last line instead of at a new line 9 | jsxBracketSameLine: false, 10 | // 行宽 11 | printWidth: 100, 12 | // 换行方式 13 | proseWrap: 'preserve', 14 | // 分号 15 | semi: false, 16 | // 使用单引号 17 | singleQuote: true, 18 | // 缩进 19 | tabWidth: 4, 20 | // 使用 tab 缩进 21 | useTabs: false, 22 | // 后置逗号,多行对象、数组在最后一行增加逗号 23 | trailingComma: 'es5', 24 | } 25 | -------------------------------------------------------------------------------- /src/res-model/failInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description res 错误信息配置 3 | * @author 双越 4 | */ 5 | 6 | module.exports = { 7 | // 登录校验失败 8 | loginCheckFailInfo: { 9 | errno: 10001, 10 | message: '登录校验失败', 11 | }, 12 | 13 | // 登录失败 14 | loginFailInfo: { 15 | errno: 10002, 16 | message: '用户名或密码错误', 17 | }, 18 | 19 | // ctx.request.body 格式验证失败 20 | validateFailInfo: { 21 | errno: 10003, 22 | message: '输入数据的格式错误', 23 | }, 24 | 25 | // 修改数据出错 26 | updateFailInfo: { 27 | errno: 10004, 28 | message: '修改数据出错,请重试', 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 工具函数 3 | * @author 双越 4 | */ 5 | 6 | const { format } = require('date-fns') 7 | 8 | /** 9 | * 字符串 '1,2' 转换为数组 [1, 2] 10 | * @param {string} ids 格式如 '1' 或 '1,2,3' 11 | */ 12 | function parseNumberArr(ids = '') { 13 | const arr = ids 14 | .split(',') 15 | .filter(i => i.length > 0) 16 | .map(i => parseInt(i, 10)) 17 | return arr 18 | } 19 | 20 | /** 21 | * 格式化时间 yyyy-MM-dd HH:mm:ss 22 | * @param {Date} d 时间 23 | */ 24 | function formatDate(d) { 25 | return format(d, 'yyyy-MM-dd HH:mm:ss') 26 | } 27 | 28 | module.exports = { 29 | parseNumberArr, 30 | formatDate, 31 | } 32 | -------------------------------------------------------------------------------- /bin/pm2-prd.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description pm2 配置文件,线上环境 3 | * @author 双越 4 | */ 5 | 6 | const appConf = require('./pm2AppConf') 7 | 8 | module.exports = { 9 | apps: [appConf], 10 | 11 | // deploy: { 12 | // production: { 13 | // user: 'SSH_USERNAME', 14 | // host: 'SSH_HOSTMACHINE', 15 | // ref: 'origin/master', 16 | // repo: 'GIT_REPOSITORY', 17 | // path: 'DESTINATION_PATH', 18 | // 'pre-deploy-local': '', 19 | // 'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production', 20 | // 'pre-setup': '', 21 | // }, 22 | // }, 23 | } 24 | -------------------------------------------------------------------------------- /src/models/AdminModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description admin model 3 | * @author 双越 4 | */ 5 | 6 | const seq = require('../db/seq/seq') 7 | const { STRING } = require('../db/seq/types') 8 | 9 | const Admin = seq.define('admin', { 10 | username: { 11 | type: STRING, 12 | allowNull: false, 13 | unique: 'username', // 不要用 `unique: true`, https://www.chaoswork.cn/1064.html 14 | comment: '用户名,唯一', 15 | }, 16 | password: { 17 | type: STRING, 18 | allowNull: false, 19 | comment: '密码', 20 | }, 21 | nickName: { 22 | type: STRING, 23 | comment: '昵称', 24 | }, 25 | }) 26 | 27 | module.exports = Admin 28 | -------------------------------------------------------------------------------- /src/validator/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 数据校验 template 3 | * @author 双越 4 | */ 5 | 6 | const templateDataSchema = { 7 | type: 'object', 8 | required: ['ids'], 9 | properties: { 10 | ids: { 11 | type: 'string', 12 | pattern: '^\\d+(,\\d+)*$', // 格式如 '1' 或 '1,2,3' 13 | }, 14 | isPublic: { 15 | type: 'boolean', 16 | }, 17 | isHot: { 18 | type: 'boolean', 19 | }, 20 | isNew: { 21 | type: 'boolean', 22 | }, 23 | orderIndex: { 24 | type: 'number', 25 | }, 26 | }, 27 | } 28 | 29 | module.exports = { 30 | templateDataSchema, 31 | } 32 | -------------------------------------------------------------------------------- /src/middlewares/cors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 跨域 中间件 3 | * @author 双越 4 | */ 5 | 6 | const cors = require('koa2-cors') 7 | const { corsOrigin } = require('../config/index') 8 | 9 | module.exports = cors({ 10 | origin: ctx => { 11 | // 非线上环境,无 cors 限制 12 | if (corsOrigin === '*') return '*' 13 | 14 | // 线上环境 15 | const ref = ctx.header.referer || '' // 如 `https://admin.imooc-logo.com/index.html` 16 | const originArr = corsOrigin.split(',').map(s => s.trim()) // 转为数组 17 | const originArrByRef = originArr.filter(s => ref.indexOf(s) === 0) // 和 ref 一致的域名 18 | if (originArrByRef.length > 0) return originArrByRef[0] 19 | 20 | // 其他情况 21 | return false 22 | }, 23 | credentials: true, // 允许跨域带 cookie 24 | }) 25 | -------------------------------------------------------------------------------- /src/db/seq/seq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 配置 sequelize ,连接 mysql 3 | * @author 双越 4 | */ 5 | 6 | const Sequelize = require('sequelize') 7 | const { mysqlConf } = require('../../config/index') 8 | const { isPrd, isTest } = require('../../utils/env') 9 | 10 | // 连接配置 11 | const { database, user, password, host, port } = mysqlConf 12 | const conf = { 13 | host, 14 | port, 15 | dialect: 'mysql', 16 | } 17 | 18 | // 测试环境不打印日志 19 | if (isTest) { 20 | conf.logging = () => {} // 默认是 console.log 21 | } 22 | 23 | // 线上环境用 链接池 24 | if (isPrd) { 25 | conf.pool = { 26 | max: 5, // 连接池中最大连接数量 27 | min: 0, // 连接池中最小连接数量 28 | idle: 10000, // 如果一个线程 10 秒钟内没有被使用过的话,那么就释放线程 29 | } 30 | } 31 | 32 | // 创建连接 33 | const seq = new Sequelize(database, user, password, conf) 34 | 35 | module.exports = seq 36 | -------------------------------------------------------------------------------- /src/utils/jwt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description jwt - verify sign 3 | * @author 双越 4 | */ 5 | 6 | const util = require('util') 7 | const jwt = require('jsonwebtoken') 8 | const { JWT_SECRET } = require('../config/constant') 9 | const { jwtExpiresIn } = require('../config/index') 10 | 11 | const verify = util.promisify(jwt.verify) 12 | 13 | /** 14 | * jwt verify 15 | * @param {string} token token 16 | */ 17 | async function jwtVerify(token) { 18 | const data = await verify(token.split(' ')[1], JWT_SECRET) // 去掉前面的 Bearer 19 | return data 20 | } 21 | 22 | /** 23 | * jwt sign 24 | * @param {Object} data data 25 | */ 26 | function jwtSign(data) { 27 | const token = jwt.sign(data, JWT_SECRET, { expiresIn: jwtExpiresIn }) 28 | return token 29 | } 30 | 31 | module.exports = { 32 | jwtVerify, 33 | jwtSign, 34 | } 35 | -------------------------------------------------------------------------------- /src/config/envs/dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description dev 配置 3 | * @author 双越 4 | */ 5 | 6 | module.exports = { 7 | // mongodb 连接配置 8 | mongodbConf: { 9 | host: 'localhost', 10 | port: '27017', 11 | dbName: 'testdb', 12 | }, 13 | 14 | // redis 连接配置 15 | redisConf: { 16 | port: '6379', 17 | host: '127.0.0.1', 18 | }, 19 | 20 | // mysql 连接配置 21 | mysqlConf: { 22 | host: 'localhost', 23 | user: 'root', 24 | password: 'xxxx', 25 | port: '3306', 26 | database: 'testdb', 27 | }, 28 | 29 | // cors origin 30 | corsOrigin: '*', 31 | 32 | // 短信验证码缓存时间,单位 s 33 | msgVeriCodeTimeout: 60, 34 | 35 | // jwt 过期时间 36 | jwtExpiresIn: '1d', // 1. 字符串,如 '1h' '2d'; 2. 数字,单位是 s 37 | 38 | // 发布出来的 h5 域名 39 | h5Origin: 'http://localhost:3001', 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description routes index 3 | * @author 双越 4 | */ 5 | 6 | const router = require('koa-router')() 7 | const { ENV } = require('../utils/env') 8 | const { cacheGet, cacheSet } = require('../cache/index') 9 | const testMysqlConn = require('../db/mysql2') 10 | const packageInfo = require('../../package.json') 11 | 12 | router.get('/api/db-check', async ctx => { 13 | // 测试 redis 14 | cacheSet('name', 'admin-server OK - by redis') 15 | const redisTestVal = await cacheGet('name') 16 | 17 | // 测试 mysql 连接 18 | const mysqlRes = await testMysqlConn() 19 | 20 | ctx.body = { 21 | errno: 0, 22 | data: { 23 | name: 'admin-server OK', 24 | version: packageInfo.version, 25 | ENV, // 测试环境量变量 26 | redisConn: redisTestVal != null, 27 | mysqlConn: mysqlRes.length > 0, 28 | }, 29 | } 30 | }) 31 | 32 | module.exports = router 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | # github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions 4 | 5 | name: test 6 | 7 | on: 8 | push: 9 | branches: 10 | - master 11 | paths: 12 | - '.github/workflows/*' 13 | - '__test__/**' 14 | - 'src/**' 15 | - 'bin/*' 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 14 27 | - name: lint and test 28 | run: | 29 | npm i 30 | npm run lint 31 | npm run test 32 | -------------------------------------------------------------------------------- /src/res-model/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description res model 3 | * @author 双越老师 4 | */ 5 | 6 | /** 7 | * 基础模型,包括 errno data 和 message 8 | */ 9 | class BaseRes { 10 | constructor({ errno, data, message }) { 11 | this.errno = errno 12 | if (data) { 13 | this.data = data 14 | } 15 | if (message) { 16 | this.message = message 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * 执行失败的数据模型 23 | */ 24 | class ErrorRes extends BaseRes { 25 | constructor({ errno = -1, message = '', data }, addMessage = '') { 26 | super({ 27 | errno, 28 | message: addMessage 29 | ? `${message} - ${addMessage}` // 有追加信息 30 | : message, 31 | data, 32 | }) 33 | } 34 | } 35 | 36 | /** 37 | * 执行成功的数据模型 38 | */ 39 | class SuccessRes extends BaseRes { 40 | constructor(data = {}) { 41 | super({ 42 | errno: 0, 43 | data, 44 | }) 45 | } 46 | } 47 | 48 | module.exports = { 49 | ErrorRes, 50 | SuccessRes, 51 | } 52 | -------------------------------------------------------------------------------- /src/middlewares/loginCheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 登录校验 3 | * @author 双越 4 | */ 5 | 6 | const { jwtVerify } = require('../utils/jwt') 7 | const { ErrorRes } = require('../res-model/index') 8 | const { loginCheckFailInfo } = require('../res-model/failInfo') 9 | 10 | /** 11 | * 登录校验 12 | * @param {Object} ctx ctx 13 | * @param {function} next next 14 | */ 15 | module.exports = async function loginCheck(ctx, next) { 16 | // 失败信息 17 | const errRes = new ErrorRes(loginCheckFailInfo) 18 | 19 | // 获取 token 20 | const token = ctx.header.authorization 21 | if (!token) { 22 | ctx.body = errRes 23 | return 24 | } 25 | 26 | let flag = true 27 | try { 28 | const userInfo = await jwtVerify(token) 29 | delete userInfo.password // 屏蔽密码 30 | 31 | // 验证成功,获取 userInfo 32 | ctx.userInfo = userInfo 33 | } catch (ex) { 34 | flag = false 35 | console.error('登录校验错误', ex) 36 | ctx.body = errRes 37 | } 38 | 39 | if (flag) { 40 | // 继续下一步 41 | await next() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 imooc-lego 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/cache/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 数据缓存 3 | * @author 双越 4 | */ 5 | 6 | const redisClient = require('../db/redis') 7 | 8 | /** 9 | * redis set 10 | * @param {string} key key 11 | * @param {string|Object} val val 12 | * @param {number} timeout 过期时间,单位 s ,默认 1h 13 | */ 14 | function cacheSet(key, val, timeout = 60 * 60) { 15 | let formatVal 16 | if (typeof val === 'object') { 17 | formatVal = JSON.stringify(val) 18 | } else { 19 | formatVal = val 20 | } 21 | redisClient.set(key, formatVal) 22 | redisClient.expire(key, timeout) 23 | } 24 | 25 | /** 26 | * redis get 27 | * @param {string} key key 28 | */ 29 | function cacheGet(key) { 30 | const promise = new Promise((resolve, reject) => { 31 | redisClient.get(key, (err, val) => { 32 | if (err) { 33 | reject(err) 34 | return 35 | } 36 | if (val == null) { 37 | resolve(null) 38 | return 39 | } 40 | 41 | try { 42 | resolve(JSON.parse(val)) 43 | } catch (ex) { 44 | resolve(val) 45 | } 46 | }) 47 | }) 48 | return promise 49 | } 50 | 51 | module.exports = { 52 | cacheSet, 53 | cacheGet, 54 | } 55 | -------------------------------------------------------------------------------- /bin/create-version-tag.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 切换到 master 分支 4 | git checkout master 5 | 6 | # 获取最新的 master 分支代码 7 | git pull origin master 8 | 9 | # npm version xxx 最后的类型,patch|minor|minor 三选一 10 | version_type="patch" ## 默认为 patch 11 | 12 | # 执行命令传入了参数(参数数量 >= 1),如 `sh ./build/up-version.sh patch` 13 | if [ $# -eq 1 -o $# -gt 1 ] 14 | then 15 | ## 参数值必须 patch|minor|major 三选一 16 | if [ $1 != "patch" -a $1 != "minor" -a $1 != "major" ] 17 | then 18 | echo "参数值错误,必须 patch|minor|major 三选一" 19 | exit 1 20 | fi 21 | version_type=$1 22 | 23 | ## 对 major 和 minor 进行再次确认 24 | if [ $version_type = 'minor' -o $version_type = 'major' ] 25 | then 26 | read -r -p "你确定要执行 npm version $version_type ? [Y/n] " input 27 | case $input in 28 | [yY][eE][sS]|[yY]) 29 | echo "确认,继续执行" 30 | ;; 31 | 32 | [nN][oO]|[nN]) 33 | echo "取消执行" 34 | exit 1 35 | ;; 36 | 37 | *) 38 | echo "非法输出,取消执行" 39 | exit 1 40 | ;; 41 | esac 42 | fi 43 | fi 44 | 45 | echo "version_type: $version_type" 46 | 47 | # 升级 npm 版本并自动生成 git tag 48 | npm version $version_type 49 | 50 | # push tags ,以触发 github actions 发布到 npm 51 | git push origin --tags 52 | -------------------------------------------------------------------------------- /src/service/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description admin service 3 | * @author 双越 4 | */ 5 | 6 | const _ = require('lodash') 7 | const AdminModel = require('../models/AdminModel') 8 | 9 | /** 10 | * 查询单个数据 11 | * @param {string} username username 12 | * @param {string} password password 13 | */ 14 | async function findOneAdminService(username, password) { 15 | // 拼接查询条件 16 | const whereOpt = {} 17 | if (username) { 18 | Object.assign(whereOpt, { username }) 19 | } 20 | if (password) { 21 | // 用户名和密码在一块,因为密码可能重复 22 | Object.assign(whereOpt, { username, password }) 23 | } 24 | 25 | // 无查询条件,则返回空 26 | if (_.isEmpty(whereOpt)) return null 27 | 28 | // 查询 29 | const result = await AdminModel.findOne({ 30 | where: whereOpt, 31 | }) 32 | if (result == null) { 33 | // 未查到用户 34 | return result 35 | } 36 | 37 | // 返回查询结果 38 | return result.dataValues 39 | } 40 | 41 | /** 42 | * 创建管理员 43 | * @param {object} adminInfo adminInfo 44 | */ 45 | async function createAdminService({ username, password, nickName = '' }) { 46 | const result = await AdminModel.create({ 47 | username, 48 | password, // 密码要加密 49 | nickName, 50 | }) 51 | return result.dataValues 52 | } 53 | 54 | module.exports = { 55 | findOneAdminService, 56 | createAdminService, 57 | } 58 | -------------------------------------------------------------------------------- /src/controller/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description admin controller 3 | * @author 双越 4 | */ 5 | 6 | const { ErrorRes, SuccessRes } = require('../res-model/index') 7 | const { loginFailInfo } = require('../res-model/failInfo') 8 | const doCrypto = require('../utils/cryp') 9 | const { jwtSign } = require('../utils/jwt') 10 | const { findOneAdminService, createAdminService } = require('../service/admin') 11 | 12 | /** 13 | * 登录 14 | * @param {string} username username 15 | * @param {string} password username 16 | */ 17 | async function login(username, password = '') { 18 | const info = await findOneAdminService( 19 | username, 20 | doCrypto(password) // 密码要加密 21 | ) 22 | if (info == null) { 23 | // 登录未成功 24 | return new ErrorRes(loginFailInfo) 25 | } 26 | // 登录成功 27 | return new SuccessRes({ 28 | token: jwtSign(info), 29 | }) 30 | } 31 | 32 | /** 33 | * 注册用户 34 | * @param {string} username username 35 | * @param {string} password username 36 | */ 37 | async function register(username, password = '') { 38 | const adminInfo = await findOneAdminService(username, doCrypto(password)) 39 | if (adminInfo != null) return adminInfo 40 | 41 | const newAdmin = await createAdminService({ username, password: doCrypto(password) }) 42 | return newAdmin 43 | } 44 | 45 | module.exports = { 46 | login, 47 | register, 48 | } 49 | -------------------------------------------------------------------------------- /src/models/UserModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description user model 3 | * @author 双越 4 | */ 5 | 6 | const seq = require('../db/seq/seq') 7 | const { STRING, DATE, BOOLEAN } = require('../db/seq/types') 8 | 9 | const User = seq.define('user', { 10 | username: { 11 | type: STRING, 12 | allowNull: false, 13 | unique: 'username', // 不要用 `unique: true`, https://www.chaoswork.cn/1064.html 14 | comment: '用户名,唯一', 15 | }, 16 | password: { 17 | type: STRING, 18 | allowNull: false, 19 | comment: '密码', 20 | }, 21 | phoneNumber: { 22 | type: STRING, 23 | allowNull: false, 24 | unique: 'username', 25 | comment: '手机号,唯一', 26 | }, 27 | nickName: { 28 | type: STRING, 29 | comment: '昵称', 30 | }, 31 | gender: { 32 | type: STRING, 33 | allowNull: false, 34 | defaultValue: 0, 35 | comment: '性别(1 男性,2 女性,0 保密)', 36 | }, 37 | picture: { 38 | type: STRING, 39 | comment: '头像,图片地址', 40 | }, 41 | city: { 42 | type: STRING, 43 | comment: '城市', 44 | }, 45 | latestLoginAt: { 46 | type: DATE, 47 | defaultValue: null, 48 | comment: '最后登录时间', 49 | }, 50 | isFrozen: { 51 | type: BOOLEAN, 52 | defaultValue: false, 53 | comment: '用户是否冻结', 54 | }, 55 | }) 56 | 57 | module.exports = User 58 | -------------------------------------------------------------------------------- /src/routes/admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description admin router 3 | * @author 双越 4 | */ 5 | 6 | const router = require('koa-router')() 7 | const { SuccessRes } = require('../res-model/index') 8 | const { isPrd } = require('../utils/env') 9 | 10 | // 中间件 11 | const loginCheck = require('../middlewares/loginCheck') 12 | const genValidator = require('../middlewares/genValidator') 13 | const { adminInfoSchema } = require('../validator/admin') 14 | 15 | // controller 16 | const { login, register } = require('../controller/admin') 17 | 18 | // 路由前缀 19 | router.prefix('/api/admin') 20 | 21 | // 获取用户信息 22 | router.get('/getUserInfo', loginCheck, async ctx => { 23 | // 经过了 loginCheck ,用户信息在 ctx.userInfo 中 24 | ctx.body = new SuccessRes(ctx.userInfo) 25 | }) 26 | 27 | // 登录 28 | router.post('/login', genValidator(adminInfoSchema), async ctx => { 29 | const { username, password } = ctx.request.body 30 | const res = await login(username, password) 31 | ctx.body = res 32 | }) 33 | 34 | // 初始化几个管理员账号 35 | ;(async function createTestAdminForDev() { 36 | if (isPrd) { 37 | // 生产环境下 38 | } else { 39 | // 非生产环境 40 | const username = 'hello' 41 | const password = '你猜?' 42 | await register(username, password) 43 | console.log( 44 | `================== 管理员账户:用户名 ${username} 密码 ${password} ==================` 45 | ) 46 | } 47 | })() 48 | 49 | module.exports = router 50 | -------------------------------------------------------------------------------- /bin/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 执行时需传入两个参数:参数1 - github 密码,参数2 - tag ,如 `sh bin/deploy.sh v1.0.1 xxx` 4 | # shell 代码中使用 $1 $2 即可依次获取参数 5 | 6 | teamPath="/home/work/lego-team" # team 目录 7 | repoPath="/home/work/lego-team/admin-server" # 项目目录,要和 repo 同名!!! 8 | repoGitUrl="https://wangfupeng1988:$1@github.com/imooc-lego/admin-server.git" 9 | 10 | if [ ! -d "$teamPath" ]; then 11 | # 如果 team 目录不存在,则创建 12 | echo =================== mkdir "$teamPath" =================== 13 | mkdir "$teamPath" 14 | fi 15 | cd "$teamPath" 16 | 17 | if [ ! -d "$repoPath" ]; then 18 | ## 如果 repo 目录不存在,则 git clone (私有项目,需要 github 用户名和密码) 19 | echo =================== git clone start =================== 20 | git clone "$repoGitUrl" 21 | git remote remove origin; # 删除 origin ,否则会暴露 github 密码 22 | echo =================== git clone end =================== 23 | fi; 24 | cd "$repoPath" 25 | 26 | git checkout . # 撤销一切文件改动,否则可能导致 pull 失败 27 | 28 | git remote add origin "$repoGitUrl" 29 | git pull origin master # 下载最新 master 代码,tag 都是基于 master 分支提交的 30 | git fetch --tags # 获取所有 tags 。否则,光执行 git pull origin master 获取不到新提交的 tag 31 | git remote remove origin; # 删除 origin ,否则会暴露 github 密码 32 | 33 | # 切换到 tag ,重要!!! 34 | git checkout "$2" 35 | echo =================== git checkout "$2" =================== 36 | 37 | # 安装依赖 38 | npm install 39 | 40 | ## 运行/重启 服务 41 | npm run prd 42 | 43 | echo =================== deploy success =================== 44 | -------------------------------------------------------------------------------- /src/middlewares/genValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 生成 ctx.request.body 格式校验的中间件 3 | * @author 双越 4 | */ 5 | 6 | const Ajv = require('ajv') 7 | const { ErrorRes } = require('../res-model/index') 8 | const { validateFailInfo } = require('../res-model/failInfo') 9 | 10 | const ajv = new Ajv({ 11 | allErrors: true, // 输出所有错误 12 | }) 13 | 14 | /** 15 | * json schema 校验 16 | * @param {Object} schema json schema 规则 17 | * @param {Object} data 待校验的数据 18 | * @returns {Array|undefined} 错误信息|undefined 19 | */ 20 | function validate(schema, data = {}) { 21 | const valid = ajv.validate(schema, data) 22 | if (!valid) { 23 | return ajv.errors 24 | } 25 | return undefined 26 | } 27 | 28 | /** 29 | * 生成校验中间件 30 | * @param {Object} schema schema 规则 31 | */ 32 | function genValidator(schema) { 33 | /** 34 | * ctx.request.body 格式校验中间件 35 | * @param {Object} ctx ctx 36 | * @param {Function} next next 37 | */ 38 | async function validator(ctx, next) { 39 | const data = ctx.request.body 40 | 41 | const validateError = validate(schema, data) 42 | if (validateError) { 43 | // 检验失败,返回 44 | ctx.body = new ErrorRes({ 45 | ...validateFailInfo, // 其中有 errno 和 message 46 | data: validateError, // 把失败信息也返回给前端 47 | }) 48 | return 49 | } 50 | // 检验成功,继续 51 | await next() 52 | } 53 | return validator 54 | } 55 | 56 | module.exports = genValidator 57 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prd.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | # github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions 4 | 5 | name: deploy production 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*.*.*' 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 12 # 尽量和线上机 node 版本保持一直 22 | - name: lint and test # 测试 23 | run: | 24 | npm i 25 | npm run lint 26 | npm run test 27 | - name: set ssh key # 临时设置 ssh key 28 | run: | 29 | mkdir -p ~/.ssh/ 30 | echo "${{secrets.WFP_ID_RSA}}" > ~/.ssh/id_rsa # secret 在这里配置 https://github.com/imooc-lego/admin-server/settings/secrets 31 | chmod 600 ~/.ssh/id_rsa 32 | ssh-keyscan "${{secrets.IP165}}" >> ~/.ssh/known_hosts 33 | - name: deploy # 部署 34 | run: ssh work@${{secrets.IP165}} "bash -s" < bin/deploy.sh ${{secrets.WFP_PASSWORD}} ${{github.ref}} 35 | - name: delete ssh key # 删除 ssh key 36 | run: rm -rf ~/.ssh/id_rsa 37 | -------------------------------------------------------------------------------- /src/routes/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description users router 3 | * @author 双越 4 | */ 5 | 6 | const router = require('koa-router')() 7 | // 中间件 8 | const loginCheck = require('../middlewares/loginCheck') 9 | const { userIdsSchema } = require('../validator/users') 10 | const genValidator = require('../middlewares/genValidator') 11 | // controller 12 | const { 13 | getUserList, 14 | updateIsFrozen, 15 | getCount, 16 | getCreatedCountMonthly, 17 | } = require('../controller/users') 18 | 19 | // 路由前缀 20 | router.prefix('/api/users') 21 | 22 | // 获取用户列表 23 | router.get('/', loginCheck, async ctx => { 24 | const { keyword = '', pageIndex, pageSize } = ctx.query 25 | const res = await getUserList(decodeURIComponent(keyword), { pageIndex, pageSize }) 26 | ctx.body = res 27 | }) 28 | 29 | // 冻结用户 30 | router.post('/froze', loginCheck, genValidator(userIdsSchema), async ctx => { 31 | const { ids } = ctx.request.body 32 | const res = await updateIsFrozen(ids, true) 33 | ctx.body = res 34 | }) 35 | 36 | // 解除冻结 37 | router.post('/unFroze', loginCheck, genValidator(userIdsSchema), async ctx => { 38 | const { ids } = ctx.request.body 39 | const res = await updateIsFrozen(ids, false) 40 | ctx.body = res 41 | }) 42 | 43 | // 获取总数 44 | router.get('/getCount', loginCheck, async ctx => { 45 | const res = await getCount() 46 | ctx.body = res 47 | }) 48 | 49 | // 按月统计 50 | router.get('/getCreatedCountMonthly', loginCheck, async ctx => { 51 | const res = await getCreatedCountMonthly() 52 | ctx.body = res 53 | }) 54 | 55 | module.exports = router 56 | -------------------------------------------------------------------------------- /src/routes/works.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description works router 3 | * @author 双越 4 | */ 5 | 6 | const router = require('koa-router')() 7 | // 中间件 8 | const loginCheck = require('../middlewares/loginCheck') 9 | const { worksIdsSchema } = require('../validator/works') 10 | const genValidator = require('../middlewares/genValidator') 11 | // controller 12 | const { 13 | getWorksList, 14 | forceOffline, 15 | undoForceOffline, 16 | getCount, 17 | getMonthlyCount, 18 | } = require('../controller/works') 19 | 20 | // 路由前缀 21 | router.prefix('/api/works') 22 | 23 | // 获取作品列表 24 | router.get('/', loginCheck, async ctx => { 25 | const { keyword = '', pageIndex, pageSize } = ctx.query 26 | const res = await getWorksList(decodeURIComponent(keyword), { pageIndex, pageSize }) 27 | ctx.body = res 28 | }) 29 | 30 | // 强制下线 31 | router.post('/forceOffline', loginCheck, genValidator(worksIdsSchema), async ctx => { 32 | const { ids } = ctx.request.body 33 | const res = await forceOffline(ids, true) 34 | ctx.body = res 35 | }) 36 | 37 | // 恢复强制下线 38 | router.post('/undoForceOffline', loginCheck, genValidator(worksIdsSchema), async ctx => { 39 | const { ids } = ctx.request.body 40 | const res = await undoForceOffline(ids, true) 41 | ctx.body = res 42 | }) 43 | 44 | // 获取发布和创建的 count 45 | router.get('/getCount', loginCheck, async ctx => { 46 | const res = await getCount() 47 | ctx.body = res 48 | }) 49 | 50 | // 按月,获取创建和发布的数量 51 | router.get('/getMonthlyCount', loginCheck, async ctx => { 52 | const res = await getMonthlyCount() 53 | ctx.body = res 54 | }) 55 | 56 | module.exports = router 57 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa') 2 | 3 | const app = new Koa() 4 | const views = require('koa-views') 5 | const json = require('koa-json') 6 | const onerror = require('koa-onerror') 7 | const bodyparser = require('koa-bodyparser') 8 | const logger = require('koa-logger') 9 | const helmet = require('koa-helmet') 10 | const cors = require('./middlewares/cors') 11 | const jwt = require('./middlewares/jwt') 12 | 13 | // 路由 14 | const index = require('./routes/index') 15 | const admin = require('./routes/admin') 16 | const users = require('./routes/users') 17 | const works = require('./routes/works') 18 | const template = require('./routes/template') 19 | 20 | // 安装预防,设置必要的 http 头 21 | app.use(helmet()) 22 | 23 | // error handler 24 | onerror(app) 25 | 26 | // 支持跨域 27 | app.use(cors) 28 | 29 | // 配置 jwt 30 | app.use(jwt) 31 | 32 | // middlewares 33 | app.use( 34 | bodyparser({ 35 | enableTypes: ['json', 'form', 'text'], 36 | }) 37 | ) 38 | app.use(json()) 39 | app.use(logger()) 40 | app.use(require('koa-static')(`${__dirname}/public`)) 41 | 42 | app.use( 43 | views(`${__dirname}/views`, { 44 | extension: 'pug', 45 | }) 46 | ) 47 | 48 | // // logger 49 | // app.use(async (ctx, next) => { 50 | // const start = new Date() 51 | // await next() 52 | // const ms = new Date() - start 53 | // console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) 54 | // }) 55 | 56 | // routes 57 | app.use(index.routes(), index.allowedMethods()) 58 | app.use(admin.routes(), admin.allowedMethods()) 59 | app.use(users.routes(), users.allowedMethods()) 60 | app.use(works.routes(), works.allowedMethods()) 61 | app.use(template.routes(), template.allowedMethods()) 62 | 63 | // error-handling 64 | app.on('error', (err, ctx) => { 65 | console.error('server error', err, ctx) 66 | }) 67 | 68 | module.exports = app 69 | -------------------------------------------------------------------------------- /src/db/seq/utils/sync-alter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 同步数据表(admins 表,其他表在 biz-editor-server 同步) 3 | * @author 双越 4 | */ 5 | 6 | const path = require('path') 7 | const simpleGit = require('simple-git') 8 | const seq = require('../seq') 9 | const { isDev } = require('../../../utils/env') 10 | 11 | // 引入 admin model ,其他的不引入 !!! 12 | require('../../../models/AdminModel') 13 | 14 | // 同步数据表 15 | async function syncDb() { 16 | let needToSyncDb = true 17 | 18 | // 只适用于开发环境!!! 19 | if (isDev) { 20 | // 开发环境下,修改频繁,每次重启都同步数据表,消耗太大 21 | // 所以,开发环境下,判断是否修改了 src/models/AdminModel 中的内容? 22 | // 如果是,则同步数据表。否则,不用同步数据表。 23 | 24 | const git = simpleGit() 25 | // 获取 git status 修改的文件,modified 格式如 [ '.gitignore', 'package.json', 'src/models/README.md' ] 26 | const { modified, not_added: nodeAdded, created, deleted, renamed } = await git.status() 27 | const fileChanged = modified 28 | .concat(nodeAdded) 29 | .concat(created) 30 | .concat(deleted) 31 | .concat(renamed) 32 | if (fileChanged.length) { 33 | // 到此,说明 git status 有改动 34 | 35 | // 是否改动了 db 相关的文件 36 | const changedDbFiles = fileChanged.some(f => { 37 | // 改动了 src/models/AdminModel ,需要同步数据库 38 | if (f.indexOf('src/models/AdminModel') === 0) return true 39 | // 改动了 src/db/seq ,需要同步数据库 40 | if (f.indexOf('src/db/seq/') === 0) return true 41 | // 其他情况,不同步 42 | return false 43 | }) 44 | // 没改动 db 文件,则不需要同步 45 | if (!changedDbFiles) needToSyncDb = false 46 | } 47 | 48 | // 如果 git status 没有改动,则照常同步数据表,重要!!! 49 | } 50 | 51 | if (needToSyncDb) { 52 | await seq.sync({ alter: true }) 53 | } 54 | } 55 | 56 | module.exports = syncDb 57 | -------------------------------------------------------------------------------- /src/routes/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description template router 3 | * @author 双越 4 | */ 5 | 6 | const router = require('koa-router')() 7 | // 中间件 8 | const loginCheck = require('../middlewares/loginCheck') 9 | const { templateDataSchema } = require('../validator/template') 10 | const genValidator = require('../middlewares/genValidator') 11 | // controller 12 | const { 13 | getTemplateList, 14 | updateIsPublic, 15 | updateIsHot, 16 | updateIsNew, 17 | updateOrderIndex, 18 | getCount, 19 | getMonthlyCount, 20 | } = require('../controller/template') 21 | 22 | // 路由前缀 23 | router.prefix('/api/template') 24 | 25 | // 获取模板列表 26 | router.get('/', loginCheck, async ctx => { 27 | const { keyword = '', pageIndex, pageSize } = ctx.query 28 | const res = await getTemplateList(decodeURIComponent(keyword), { pageIndex, pageSize }) 29 | ctx.body = res 30 | }) 31 | 32 | // 设置 isPublic 33 | router.patch('/isPublic', loginCheck, genValidator(templateDataSchema), async ctx => { 34 | const { ids, isPublic } = ctx.request.body 35 | const res = await updateIsPublic(ids, isPublic) 36 | ctx.body = res 37 | }) 38 | 39 | // 设置 isHot 40 | router.patch('/isHot', loginCheck, genValidator(templateDataSchema), async ctx => { 41 | const { ids, isHot } = ctx.request.body 42 | const res = await updateIsHot(ids, isHot) 43 | ctx.body = res 44 | }) 45 | 46 | // 设置 isNew 47 | router.patch('/isNew', loginCheck, genValidator(templateDataSchema), async ctx => { 48 | const { ids, isNew } = ctx.request.body 49 | const res = await updateIsNew(ids, isNew) 50 | ctx.body = res 51 | }) 52 | 53 | // 设置 orderIndex 54 | router.patch('/orderIndex', loginCheck, genValidator(templateDataSchema), async ctx => { 55 | const { ids, orderIndex } = ctx.request.body 56 | const res = await updateOrderIndex(ids, orderIndex) 57 | ctx.body = res 58 | }) 59 | 60 | // 获取总数 61 | router.get('/getCount', loginCheck, async ctx => { 62 | const res = await getCount() 63 | ctx.body = res 64 | }) 65 | 66 | // 按月统计 67 | router.get('/getMonthlyCount', loginCheck, async ctx => { 68 | const res = await getMonthlyCount() 69 | ctx.body = res 70 | }) 71 | 72 | module.exports = router 73 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../src/app') 8 | var debug = require('debug')('demo:server') 9 | var http = require('http') 10 | var syncDb = require('../src/db/seq/utils/sync-alter') 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | 16 | var port = normalizePort(process.env.PORT || '3003') 17 | // app.set('port', port); 18 | console.log('port...', port) 19 | 20 | /** 21 | * Create HTTP server. 22 | */ 23 | 24 | var server = http.createServer(app.callback()) 25 | 26 | /** 27 | * Listen on provided port, on all network interfaces. 28 | */ 29 | 30 | // 先同步 mysql 数据表 31 | syncDb().then(() => { 32 | // 再启动服务 33 | server.listen(port) 34 | server.on('error', onError) 35 | server.on('listening', onListening) 36 | }) 37 | 38 | /** 39 | * Normalize a port into a number, string, or false. 40 | */ 41 | 42 | function normalizePort(val) { 43 | var port = parseInt(val, 10) 44 | 45 | if (isNaN(port)) { 46 | // named pipe 47 | return val 48 | } 49 | 50 | if (port >= 0) { 51 | // port number 52 | return port 53 | } 54 | 55 | return false 56 | } 57 | 58 | /** 59 | * Event listener for HTTP server "error" event. 60 | */ 61 | 62 | function onError(error) { 63 | if (error.syscall !== 'listen') { 64 | throw error 65 | } 66 | 67 | var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port 68 | 69 | // handle specific listen errors with friendly messages 70 | switch (error.code) { 71 | case 'EACCES': 72 | console.error(bind + ' requires elevated privileges') 73 | process.exit(1) 74 | break 75 | case 'EADDRINUSE': 76 | console.error(bind + ' is already in use') 77 | process.exit(1) 78 | break 79 | default: 80 | throw error 81 | } 82 | } 83 | 84 | /** 85 | * Event listener for HTTP server "listening" event. 86 | */ 87 | 88 | function onListening() { 89 | var addr = server.address() 90 | var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port 91 | debug('Listening on ' + bind) 92 | } 93 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | package-lock.json 107 | 108 | ISSUE.md 109 | 110 | wiki/ 111 | 112 | wfp_test/ 113 | -------------------------------------------------------------------------------- /src/models/WorksModel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 作品 Model 3 | * @author 双越 4 | */ 5 | 6 | const seq = require('../db/seq/seq') 7 | const { INTEGER, STRING, BOOLEAN, DATE } = require('../db/seq/types') 8 | 9 | // 作品 10 | const Work = seq.define('work', { 11 | uuid: { 12 | type: STRING, 13 | allowNull: false, 14 | unique: 'uuid', 15 | comment: 'uuid,h5 url 中使用,隐藏真正的 id,避免被爬虫', 16 | }, 17 | title: { 18 | type: STRING, 19 | allowNull: false, 20 | comment: '标题', 21 | }, 22 | desc: { 23 | type: STRING, 24 | comment: '副标题', 25 | }, 26 | contentId: { 27 | type: STRING, 28 | allowNull: false, 29 | unique: 'contentId', 30 | comment: '内容 id ,内容存储在 mongodb 中', 31 | }, 32 | publishContentId: { 33 | type: STRING, 34 | unique: 'publishContentId', 35 | comment: '发布内容 id ,内容存储在 mongodb 中,未发布的为空', 36 | }, 37 | author: { 38 | type: STRING, 39 | allowNull: false, 40 | comment: '作者 username', 41 | }, 42 | coverImg: { 43 | type: STRING, 44 | comment: '封面图片 url', 45 | }, 46 | isTemplate: { 47 | type: BOOLEAN, 48 | allowNull: false, 49 | defaultValue: false, 50 | comment: '是否是模板', 51 | }, 52 | status: { 53 | type: INTEGER, 54 | allowNull: false, 55 | defaultValue: 1, 56 | comment: '状态:0-删除,1-未发布,2-发布,3-强制下线', 57 | }, 58 | // viewedCount: { 59 | // type: INTEGER, 60 | // allowNull: false, 61 | // defaultValue: 0, 62 | // comment: '被浏览次数', 63 | // }, 64 | copiedCount: { 65 | type: INTEGER, 66 | allowNull: false, 67 | defaultValue: 0, 68 | comment: '被复制的次数', 69 | }, 70 | latestPublishAt: { 71 | type: DATE, 72 | defaultValue: null, 73 | comment: '最后一次发布的时间', 74 | }, 75 | isHot: { 76 | type: BOOLEAN, 77 | defaultValue: false, 78 | comment: 'hot 标签,模板使用', 79 | }, 80 | isNew: { 81 | type: BOOLEAN, 82 | defaultValue: false, 83 | comment: 'new 标签,模板使用', 84 | }, 85 | orderIndex: { 86 | type: INTEGER, 87 | defaultValue: 0, 88 | comment: '排序参数', 89 | }, 90 | isPublic: { 91 | type: BOOLEAN, 92 | defaultValue: false, 93 | comment: '是否公开显示,在首页公共的模板列表', 94 | }, 95 | }) 96 | 97 | module.exports = Work 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-server-open", 3 | "version": "1.0.0", 4 | "description": "后台管理服务端", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node bin/www", 8 | "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www --trace-warnings", 9 | "prd-dev": "cross-env NODE_ENV=prd_dev pm2 start bin/pm2-prd-dev.config.js", 10 | "prd": "cross-env NODE_ENV=production NODE_LOG_DIR=/tmp/admin-server ENABLE_NODE_LOG=YES pm2 start bin/pm2-prd.config.js", 11 | "test": "cross-env NODE_ENV=test jest --runInBand --passWithNoTests --colors", 12 | "lint": "eslint \"src/**/*.{js,ts}\"", 13 | "lint-fix": "eslint --fix \"src/**/*.{js,ts}\"" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imooc-lego/admin-server.git" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/imooc-lego/admin-server/issues" 23 | }, 24 | "homepage": "https://github.com/imooc-lego/admin-server#readme", 25 | "devDependencies": { 26 | "eslint": "^7.8.1", 27 | "eslint-config-airbnb-base": "^14.2.0", 28 | "eslint-config-prettier": "^6.11.0", 29 | "eslint-plugin-import": "^2.22.0", 30 | "eslint-plugin-prettier": "^3.1.4", 31 | "husky": "^4.3.0", 32 | "jest": "^26.4.2", 33 | "lint-staged": "^10.3.0", 34 | "nodemon": "^1.8.1", 35 | "prettier": "^2.1.1" 36 | }, 37 | "dependencies": { 38 | "ajv": "^6.12.5", 39 | "cos-nodejs-sdk-v5": "^2.7.2", 40 | "cross-env": "^7.0.2", 41 | "date-fns": "^2.16.1", 42 | "debug": "^2.6.3", 43 | "jsonwebtoken": "^8.5.1", 44 | "koa": "^2.2.0", 45 | "koa-bodyparser": "^3.2.0", 46 | "koa-convert": "^1.2.0", 47 | "koa-helmet": "^6.0.0", 48 | "koa-json": "^2.0.2", 49 | "koa-jwt": "^4.0.0", 50 | "koa-logger": "^2.0.1", 51 | "koa-onerror": "^1.2.1", 52 | "koa-router": "^7.1.1", 53 | "koa-static": "^3.0.0", 54 | "koa-views": "^5.2.1", 55 | "koa2-cors": "^2.0.6", 56 | "lodash": "^4.17.20", 57 | "mysql2": "^2.1.0", 58 | "pm2": "^4.4.1", 59 | "prompt": "^1.0.0", 60 | "pug": "^2.0.0-rc.1", 61 | "redis": "^3.0.2", 62 | "sequelize": "^6.3.5", 63 | "simple-git": "^2.21.0", 64 | "uuid": "^8.3.1" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/deploy-dev.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | # github actions 中文文档 https://docs.github.com/cn/actions/getting-started-with-github-actions 4 | 5 | name: deploy for dev 6 | 7 | on: 8 | push: 9 | branches: 10 | - 'dev' # 只针对 dev 分支 11 | paths: 12 | - '.github/workflows/*' 13 | - '__test__/**' 14 | - 'src/**' 15 | - 'Dockerfile' 16 | - 'docker-compose.yml' 17 | - 'bin/*' 18 | 19 | jobs: 20 | deploy: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: 14 29 | - name: lint and test # 测试 30 | run: | 31 | npm i 32 | npm run lint 33 | npm run test 34 | - name: set ssh key # 临时设置 ssh key 35 | run: | 36 | mkdir -p ~/.ssh/ 37 | echo "${{secrets.WFP_ID_RSA}}" > ~/.ssh/id_rsa # secret 在这里配置 https://github.com/imooc-lego/admin-server/settings/secrets 38 | chmod 600 ~/.ssh/id_rsa 39 | ssh-keyscan "182.92.xxx.xxx" >> ~/.ssh/known_hosts 40 | - name: deploy # 部署 41 | run: | 42 | ssh work@182.92.xxx.xxx " 43 | # 【注意】手动创建 /home/work/imooc-lego 目录 44 | # 然后 git clone https://username:password@github.com/imooc-lego/admin-server.git -b dev (私有项目,需要 github 用户名和密码) 45 | # 记得删除 origin ,否则会暴露 github 密码 46 | 47 | cd /home/work/imooc-lego/admin-server; 48 | git remote add origin https://wangfupeng1988:${{secrets.WFP_PASSWORD}}@github.com/imooc-lego/admin-server.git; 49 | git checkout dev; 50 | git pull origin dev; # 重新下载最新代码 51 | git remote remove origin; # 删除 origin ,否则会暴露 github 密码 52 | 53 | # 启动 docker 54 | docker-compose build admin-server; # 和 docker-compose.yml service 名字一致 55 | docker-compose up -d; 56 | " 57 | - name: delete ssh key # 删除 ssh key 58 | run: rm -rf ~/.ssh/id_rsa 59 | -------------------------------------------------------------------------------- /src/service/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description users service 3 | * @author 双越 4 | */ 5 | 6 | const _ = require('lodash') 7 | const UserModel = require('../models/UserModel') 8 | const seq = require('../db/seq/seq') 9 | const { formatDate } = require('../utils/util') 10 | 11 | /** 12 | * 查询用户列表 13 | * @param {object} whereOpt 查询条件 14 | * @param {object} pageOpt 分页数据 15 | */ 16 | async function findUsersService(whereOpt = {}, pageOpt = {}) { 17 | const { pageSize, pageIndex } = pageOpt 18 | const pageSizeNumber = parseInt(pageSize, 10) // 有可能传入进来是 string 类型的 19 | const pageIndexNumber = parseInt(pageIndex, 10) 20 | 21 | const result = await UserModel.findAndCountAll({ 22 | limit: pageSizeNumber, // 每页多少条 23 | offset: pageSizeNumber * pageIndexNumber, // 跳过多少条 24 | order: [ 25 | ['id', 'desc'], // id 倒叙 26 | ], 27 | where: whereOpt, 28 | }) 29 | // result.count 总数,忽略了 limit 和 offset 30 | // result.rows 查询结果,数组 31 | const list = result.rows.map(row => row.dataValues) 32 | 33 | return { 34 | count: result.count, 35 | list, 36 | } 37 | } 38 | 39 | /** 40 | * 修改用户 41 | * @param {object} updateData 要修改的数据 42 | * @param {object} whereOpt 条件 43 | */ 44 | async function updateUsersService(updateData = {}, whereOpt = {}) { 45 | // 校验数据 46 | if (_.isEmpty(updateData)) return false 47 | if (_.isEmpty(whereOpt)) return false 48 | 49 | const result = await UserModel.update(updateData, { where: whereOpt }) 50 | 51 | return result[0] !== 0 52 | } 53 | 54 | /** 55 | * 获取总数 56 | * @param {object} whereOpt 条件 57 | */ 58 | async function getUsersCountService(whereOpt = {}) { 59 | const result = await UserModel.count({ where: whereOpt }) 60 | return result 61 | } 62 | 63 | /** 64 | * 按月,统计创建数量 65 | * @param {Date} startTime 开始时间 66 | * @param {Date} endTime 结束时间 67 | */ 68 | async function getCreatedCountMonthlyService(startTime, endTime) { 69 | // 使用世界标准时区 70 | const startTimeStr = formatDate(startTime) 71 | const endTimeStr = formatDate(endTime) 72 | 73 | const sql = `select count(id) as \`count\`, DATE_FORMAT(createdAt, '%Y-%m') as \`month\` 74 | from users 75 | where createdAt >= '${startTimeStr}' and createdAt <= '${endTimeStr}' 76 | group by \`month\`;` 77 | 78 | const result = await seq.query(sql) 79 | if (result.length === 2) return result[0] 80 | return result 81 | } 82 | 83 | module.exports = { 84 | findUsersService, 85 | updateUsersService, 86 | getUsersCountService, 87 | getCreatedCountMonthlyService, 88 | } 89 | -------------------------------------------------------------------------------- /src/service/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description template service 3 | * @author 双越 4 | */ 5 | 6 | const { findWorksService, updateWorksService, getWorksCountService } = require('./works') 7 | const seq = require('../db/seq/seq') 8 | const { formatDate } = require('../utils/util') 9 | 10 | /** 11 | * 查询 作品/模板 列表 12 | * @param {object} whereOpt 查询条件 13 | * @param {object} pageOpt 分页数据 14 | */ 15 | async function findTemplateService(whereOpt = {}, pageOpt = {}) { 16 | const res = await findWorksService( 17 | { 18 | ...whereOpt, 19 | isTemplate: true, // 是模板 20 | }, 21 | pageOpt 22 | ) 23 | return res 24 | } 25 | 26 | /** 27 | * 修改 作品/模板 28 | * @param {object} updateData 要修改的数据 29 | * @param {object} whereOpt 条件 30 | */ 31 | async function updateTemplateService(updateData = {}, whereOpt = {}) { 32 | const res = await updateWorksService(updateData, { 33 | ...whereOpt, 34 | isTemplate: true, // 是模板 35 | }) 36 | return res 37 | } 38 | 39 | /** 40 | * 获取总数 41 | * @param {object} whereOpt 条件 42 | */ 43 | async function getTemplateCountService(whereOpt = {}) { 44 | const res = await getWorksCountService({ 45 | ...whereOpt, 46 | isTemplate: true, // 是模板 47 | }) 48 | return res 49 | } 50 | 51 | /** 52 | * 获取使用次数的综合 53 | */ 54 | async function getCopiedCountService() { 55 | const sql = `select sum(copiedCount) as \`copiedCount\` from works where isTemplate=1` 56 | const result = await seq.query(sql) 57 | if (result.length === 2) return result[0] 58 | return result 59 | } 60 | 61 | /** 62 | * 按月,统计创建数量 63 | * @param {Date} startTime 开始时间 64 | * @param {Date} endTime 结束时间 65 | */ 66 | async function getCreatedCountMonthlyService(startTime, endTime) { 67 | // 使用世界标准时区 68 | const startTimeStr = formatDate(startTime) 69 | const endTimeStr = formatDate(endTime) 70 | 71 | const sql = `select count(id) as \`count\`, DATE_FORMAT(latestPublishAt, '%Y-%m') as \`month\` 72 | from works 73 | where isTemplate=1 && latestPublishAt >= '${startTimeStr}' and latestPublishAt <= '${endTimeStr}' 74 | group by \`month\`;` 75 | 76 | const result = await seq.query(sql) 77 | if (result.length === 2) return result[0] 78 | return result 79 | } 80 | 81 | /** 82 | * 按月,统计发布数量 83 | * @param {Date} startTime 开始时间 84 | * @param {Date} endTime 结束时间 85 | */ 86 | async function getCopiedCountMonthlyService(startTime, endTime) { 87 | // 使用世界标准时区 88 | const startTimeStr = formatDate(startTime) 89 | const endTimeStr = formatDate(endTime) 90 | 91 | const sql = `select sum(copiedCount) as \`copiedCount\`, DATE_FORMAT(latestPublishAt, '%Y-%m') as \`month\` 92 | from works 93 | where isTemplate=1 and latestPublishAt >= '${startTimeStr}' and latestPublishAt <= '${endTimeStr}' 94 | group by \`month\`;` 95 | 96 | const result = await seq.query(sql) 97 | if (result.length === 2) return result[0] 98 | return result 99 | } 100 | 101 | module.exports = { 102 | findTemplateService, 103 | updateTemplateService, 104 | getTemplateCountService, 105 | getCopiedCountService, 106 | getCreatedCountMonthlyService, 107 | getCopiedCountMonthlyService, 108 | } 109 | -------------------------------------------------------------------------------- /src/controller/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description users controller 3 | * @author 双越 4 | */ 5 | 6 | const { Op } = require('sequelize') 7 | const { 8 | findUsersService, 9 | updateUsersService, 10 | getUsersCountService, 11 | getCreatedCountMonthlyService, 12 | } = require('../service/users') 13 | const { SuccessRes, ErrorRes } = require('../res-model/index') 14 | const { DEFAULT_PAGE_SIZE } = require('../config/constant') 15 | const { updateFailInfo } = require('../res-model/failInfo') 16 | const { parseNumberArr } = require('../utils/util') 17 | 18 | /** 19 | * 查询用户列表 20 | * @param {string} keyword 搜索关键字 21 | * @param {object} pageOpt 分页 22 | */ 23 | async function getUserList(keyword = '', pageOpt = {}) { 24 | // 查询条件 25 | let whereOpt = {} 26 | if (keyword) { 27 | const keywordOpt = { [Op.like]: `%${keyword}%` } 28 | whereOpt = { 29 | [Op.or]: [ 30 | // 所有查询条件, or 拼接 31 | { id: keywordOpt }, 32 | { username: keywordOpt }, 33 | { phoneNumber: keywordOpt }, 34 | { nickName: keywordOpt }, 35 | ], 36 | } 37 | } 38 | 39 | // 分页 40 | let { pageIndex, pageSize } = pageOpt 41 | pageIndex = parseInt(pageIndex, 10) || 0 42 | pageSize = parseInt(pageSize, 10) || DEFAULT_PAGE_SIZE 43 | 44 | // 执行查询 45 | const { list, count } = await findUsersService(whereOpt, { pageIndex, pageSize }) 46 | 47 | // 屏蔽掉密码 48 | const listWithoutPassword = list.map(u => { 49 | const u1 = u 50 | delete u1.password 51 | return u1 52 | }) 53 | 54 | return new SuccessRes({ list: listWithoutPassword, count }) 55 | } 56 | 57 | /** 58 | * 修改 isFrozen 59 | * @param {string} ids 如 '1' '1,2,3' 60 | * @param {boolean} isFrozen 是否冻结 61 | */ 62 | async function updateIsFrozen(ids = '', isFrozen = false) { 63 | // 将 ids 字符串变为数组 64 | const idsArr = parseNumberArr(ids) 65 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 66 | 67 | // 更新 68 | const result = await updateUsersService( 69 | { isFrozen }, 70 | { 71 | id: { 72 | [Op.in]: idsArr, 73 | }, 74 | } 75 | ) 76 | 77 | if (result) return new SuccessRes() 78 | return new ErrorRes(updateFailInfo) 79 | } 80 | 81 | /** 82 | * 获取总数 83 | */ 84 | async function getCount() { 85 | // 总数 86 | const count = await getUsersCountService() 87 | 88 | // 活跃人数,最后登录时间在 3 个月以内的 89 | const d = new Date() 90 | const limitDate = new Date(d.getTime() - 90 * 24 * 60 * 60 * 1000) // 90 天之前 91 | const active = await getUsersCountService({ 92 | latestLoginAt: { 93 | [Op.gte]: limitDate, 94 | }, 95 | }) 96 | 97 | return new SuccessRes({ 98 | count, 99 | active, 100 | }) 101 | } 102 | 103 | /** 104 | * 按月统计创建量 105 | */ 106 | async function getCreatedCountMonthly() { 107 | // 一年的时间范围 108 | const d = new Date() 109 | const startTime = new Date(d.getTime() - 365 * 24 * 60 * 60 * 1000) // 一年之前 110 | const endTime = d 111 | 112 | const result = await getCreatedCountMonthlyService(startTime, endTime) 113 | 114 | return new SuccessRes(result) 115 | } 116 | 117 | module.exports = { 118 | getUserList, 119 | updateIsFrozen, 120 | getCount, 121 | getCreatedCountMonthly, 122 | } 123 | -------------------------------------------------------------------------------- /src/service/works.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description works and template service 3 | * @author 双越 4 | */ 5 | 6 | const _ = require('lodash') 7 | const WorksModel = require('../models/WorksModel') 8 | const seq = require('../db/seq/seq') 9 | const { formatDate } = require('../utils/util') 10 | 11 | /** 12 | * 查询 作品/模板 列表 13 | * @param {object} whereOpt 查询条件 14 | * @param {object} pageOpt 分页数据 15 | */ 16 | async function findWorksService(whereOpt = {}, pageOpt = {}) { 17 | const { pageSize, pageIndex } = pageOpt 18 | const pageSizeNumber = parseInt(pageSize, 10) // 有可能传入进来是 string 类型的 19 | const pageIndexNumber = parseInt(pageIndex, 10) 20 | 21 | const result = await WorksModel.findAndCountAll({ 22 | limit: pageSizeNumber, // 每页多少条 23 | offset: pageSizeNumber * pageIndexNumber, // 跳过多少条 24 | order: [ 25 | ['orderIndex', 'desc'], // 倒序 26 | ['id', 'desc'], // 倒序。多个排序,按先后顺序确定优先级 27 | ], 28 | where: whereOpt, 29 | }) 30 | // result.count 总数,忽略了 limit 和 offset 31 | // result.rows 查询结果,数组 32 | const list = result.rows.map(row => row.dataValues) 33 | 34 | return { 35 | count: result.count, 36 | list, 37 | } 38 | } 39 | 40 | /** 41 | * 修改 作品/模板 42 | * @param {object} updateData 要修改的数据 43 | * @param {object} whereOpt 条件 44 | */ 45 | async function updateWorksService(updateData = {}, whereOpt = {}) { 46 | // 校验数据 47 | if (_.isEmpty(updateData)) return false 48 | if (_.isEmpty(whereOpt)) return false 49 | 50 | const result = await WorksModel.update(updateData, { where: whereOpt }) 51 | 52 | return result[0] !== 0 53 | } 54 | 55 | /** 56 | * 获取总数 57 | * @param {object} whereOpt 条件 58 | */ 59 | async function getWorksCountService(whereOpt = {}) { 60 | const result = await WorksModel.count({ where: whereOpt }) 61 | return result 62 | } 63 | 64 | /** 65 | * 按月,统计创建数量 66 | * @param {Date} startTime 开始时间 67 | * @param {Date} endTime 结束时间 68 | */ 69 | async function getCreatedCountMonthlyService(startTime, endTime) { 70 | // 使用世界标准时区 71 | const startTimeStr = formatDate(startTime) 72 | const endTimeStr = formatDate(endTime) 73 | 74 | const sql = `select count(id) as \`count\`, DATE_FORMAT(createdAt, '%Y-%m') as \`month\` 75 | from works 76 | where createdAt >= '${startTimeStr}' and createdAt <= '${endTimeStr}' 77 | group by \`month\`;` 78 | 79 | const result = await seq.query(sql) 80 | if (result.length === 2) return result[0] 81 | return result 82 | } 83 | 84 | /** 85 | * 按月,统计发布数量 86 | * @param {Date} startTime 开始时间 87 | * @param {Date} endTime 结束时间 88 | */ 89 | async function getPublishedCountMonthlyService(startTime, endTime) { 90 | // 使用世界标准时区 91 | const startTimeStr = formatDate(startTime) 92 | const endTimeStr = formatDate(endTime) 93 | 94 | const sql = `select count(id) as \`count\`, DATE_FORMAT(latestPublishAt, '%Y-%m') as \`month\` 95 | from works 96 | where status=2 and latestPublishAt >= '${startTimeStr}' and latestPublishAt <= '${endTimeStr}' 97 | group by \`month\`;` 98 | 99 | const result = await seq.query(sql) 100 | if (result.length === 2) return result[0] 101 | return result 102 | } 103 | 104 | module.exports = { 105 | findWorksService, 106 | updateWorksService, 107 | getWorksCountService, 108 | getCreatedCountMonthlyService, 109 | getPublishedCountMonthlyService, 110 | } 111 | -------------------------------------------------------------------------------- /src/controller/works.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description works controller 3 | * @author 双越 4 | */ 5 | 6 | const { Op } = require('sequelize') 7 | const { 8 | findWorksService, 9 | updateWorksService, 10 | getWorksCountService, 11 | getCreatedCountMonthlyService, 12 | getPublishedCountMonthlyService, 13 | } = require('../service/works') 14 | const { SuccessRes, ErrorRes } = require('../res-model/index') 15 | const { DEFAULT_PAGE_SIZE } = require('../config/constant') 16 | const { updateFailInfo } = require('../res-model/failInfo') 17 | const { publishWorkClearCache } = require('../cache/works/publish') 18 | const { parseNumberArr } = require('../utils/util') 19 | 20 | /** 21 | * 查询作品列表 22 | * @param {string} keyword 搜索关键字 23 | * @param {object} pageOpt 分页 24 | */ 25 | async function getWorksList(keyword = '', pageOpt = {}) { 26 | // 查询条件 27 | let whereOpt = {} 28 | if (keyword) { 29 | const keywordOpt = { [Op.like]: `%${keyword}%` } 30 | whereOpt = { 31 | [Op.or]: [ 32 | // 所有查询条件, or 拼接 33 | { id: keywordOpt }, 34 | { title: keywordOpt }, 35 | { author: keywordOpt }, 36 | ], 37 | } 38 | } 39 | 40 | // 分页 41 | let { pageIndex, pageSize } = pageOpt 42 | pageIndex = parseInt(pageIndex, 10) || 0 43 | pageSize = parseInt(pageSize, 10) || DEFAULT_PAGE_SIZE 44 | 45 | // 执行查询 46 | const { list, count } = await findWorksService(whereOpt, { pageIndex, pageSize }) 47 | 48 | return new SuccessRes({ list, count }) 49 | } 50 | 51 | /** 52 | * 强制下线 53 | * @param {string}} ids 如 '1' '1,2,3' 54 | */ 55 | async function forceOffline(ids = '') { 56 | // 将 ids 字符串变为数组 57 | const idsArr = parseNumberArr(ids) 58 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 59 | 60 | // 更新 61 | const result = await updateWorksService( 62 | { 63 | status: 3, // 3 即为强制下线 64 | }, 65 | { 66 | id: { 67 | [Op.in]: idsArr, 68 | }, 69 | status: 2, // 只有发布状态,才能强制下线 70 | } 71 | ) 72 | 73 | if (!result) return new ErrorRes(updateFailInfo) 74 | 75 | // 清空 h5 的缓存 76 | idsArr.forEach(id => publishWorkClearCache(id)) 77 | 78 | return new SuccessRes() 79 | } 80 | 81 | /** 82 | * 恢复 83 | * @param {string}} ids 如 '1' '1,2,3' 84 | */ 85 | async function undoForceOffline(ids = '') { 86 | // 将 ids 字符串变为数组 87 | const idsArr = parseNumberArr(ids) 88 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 89 | 90 | // 更新 91 | const result = await updateWorksService( 92 | { 93 | status: 2, // 2 即正常上线 94 | }, 95 | { 96 | id: { 97 | [Op.in]: idsArr, 98 | }, 99 | status: 3, // 只有强制下线状态,才能恢复为上线 100 | } 101 | ) 102 | 103 | if (result) return new SuccessRes() 104 | return new ErrorRes(updateFailInfo) 105 | } 106 | 107 | /** 108 | * 获取创建和发布的总数 109 | */ 110 | async function getCount() { 111 | const created = await getWorksCountService() 112 | const published = await getWorksCountService({ status: 2 }) 113 | return new SuccessRes({ 114 | created, 115 | published, 116 | }) 117 | } 118 | 119 | /** 120 | * 获取每月创建和发布的总数 121 | */ 122 | async function getMonthlyCount() { 123 | // 一年的时间范围 124 | const d = new Date() 125 | const startTime = new Date(d.getTime() - 365 * 24 * 60 * 60 * 1000) // 一年之前 126 | const endTime = d 127 | 128 | // 获取每月创建的数量 129 | const createdResult = await getCreatedCountMonthlyService(startTime, endTime) 130 | 131 | // 获取每月发布的数量 132 | const publishedResult = await getPublishedCountMonthlyService(startTime, endTime) 133 | 134 | // 格式化数据 135 | const resDataObj = {} 136 | createdResult.forEach(item => { 137 | const { month, count = 0 } = item || {} 138 | if (!month) return 139 | if (resDataObj[month] == null) resDataObj[month] = {} 140 | 141 | const data = resDataObj[month] 142 | data.created = count 143 | if (data.published == null) data.published = 0 144 | }) 145 | publishedResult.forEach(item => { 146 | const { month, count = 0 } = item || {} 147 | if (!month) return 148 | if (resDataObj[month] == null) resDataObj[month] = {} 149 | 150 | const data = resDataObj[month] 151 | data.published = count 152 | if (data.created == null) data.created = 0 153 | }) 154 | const monthKeys = Object.keys(resDataObj) 155 | const resData = monthKeys.map(month => { 156 | return { 157 | month, 158 | data: resDataObj[month], 159 | } 160 | }) 161 | 162 | return new SuccessRes(resData) 163 | } 164 | 165 | module.exports = { 166 | getWorksList, 167 | forceOffline, 168 | undoForceOffline, 169 | getCount, 170 | getMonthlyCount, 171 | } 172 | -------------------------------------------------------------------------------- /src/controller/template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description template controller 3 | * @author 双越 4 | */ 5 | 6 | const { Op } = require('sequelize') 7 | const { 8 | findTemplateService, 9 | updateTemplateService, 10 | getTemplateCountService, 11 | getCopiedCountService, 12 | getCreatedCountMonthlyService, 13 | getCopiedCountMonthlyService, 14 | } = require('../service/template') 15 | const { SuccessRes, ErrorRes } = require('../res-model/index') 16 | const { DEFAULT_PAGE_SIZE } = require('../config/constant') 17 | const { updateFailInfo } = require('../res-model/failInfo') 18 | const { parseNumberArr } = require('../utils/util') 19 | 20 | /** 21 | * 更新模板 22 | * @param {object} data 要更新的数据 23 | * @param {Array} idArr id 数组 24 | */ 25 | async function updateTemplate(data = {}, idsArr = []) { 26 | const result = await updateTemplateService(data, { 27 | id: { 28 | [Op.in]: idsArr, 29 | }, 30 | }) 31 | 32 | if (result) return new SuccessRes() 33 | return new ErrorRes(updateFailInfo) 34 | } 35 | 36 | /** 37 | * 查询模板列表 38 | * @param {string} keyword 搜索关键字 39 | * @param {object} pageOpt 分页 40 | */ 41 | async function getTemplateList(keyword = '', pageOpt = {}) { 42 | // 查询条件 43 | let whereOpt = {} 44 | if (keyword) { 45 | const keywordOpt = { [Op.like]: `%${keyword}%` } 46 | whereOpt = { 47 | [Op.or]: [ 48 | // 所有查询条件, or 拼接 49 | { id: keywordOpt }, 50 | { title: keywordOpt }, 51 | { author: keywordOpt }, 52 | ], 53 | } 54 | } 55 | 56 | // 分页 57 | let { pageIndex, pageSize } = pageOpt 58 | pageIndex = parseInt(pageIndex, 10) || 0 59 | pageSize = parseInt(pageSize, 10) || DEFAULT_PAGE_SIZE 60 | 61 | // 执行查询 62 | const { list, count } = await findTemplateService(whereOpt, { pageIndex, pageSize }) 63 | 64 | return new SuccessRes({ list, count }) 65 | } 66 | 67 | /** 68 | * 设置 isPublic 69 | * @param {string} ids 如 '1' '1,2,3' 70 | * @param {boolean} isPublic isPublic 71 | */ 72 | async function updateIsPublic(ids = '', isPublic = false) { 73 | // 将 ids 字符串变为数组 74 | const idsArr = parseNumberArr(ids) 75 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 76 | 77 | // 更新 78 | const res = await updateTemplate({ isPublic }, idsArr) 79 | return res 80 | } 81 | 82 | /** 83 | * 设置 isHot 84 | * @param {string} ids 如 '1' '1,2,3' 85 | * @param {boolean} isHot isHot 86 | */ 87 | async function updateIsHot(ids = '', isHot = false) { 88 | // 将 ids 字符串变为数组 89 | const idsArr = parseNumberArr(ids) 90 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 91 | 92 | // 更新 93 | const res = await updateTemplate({ isHot }, idsArr) 94 | return res 95 | } 96 | 97 | /** 98 | * 设置 isNew 99 | * @param {string} ids 如 '1' '1,2,3' 100 | * @param {boolean} isNew isNew 101 | */ 102 | async function updateIsNew(ids = '', isNew = false) { 103 | // 将 ids 字符串变为数组 104 | const idsArr = parseNumberArr(ids) 105 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 106 | 107 | // 更新 108 | const res = await updateTemplate({ isNew }, idsArr) 109 | return res 110 | } 111 | 112 | /** 113 | * 设置 orderIndex 114 | * @param {string} ids 如 '1' '1,2,3' 115 | * @param {number} orderIndex orderIndex 116 | */ 117 | async function updateOrderIndex(ids = '', orderIndex = 0) { 118 | // 将 ids 字符串变为数组 119 | const idsArr = parseNumberArr(ids) 120 | if (idsArr.length === 0) return new ErrorRes(updateFailInfo) 121 | 122 | // 更新 123 | const res = await updateTemplate({ orderIndex }, idsArr) 124 | return res 125 | } 126 | 127 | /** 128 | * 获取总数 129 | */ 130 | async function getCount() { 131 | const count = await getTemplateCountService() 132 | const copiedResult = await getCopiedCountService() 133 | const obj = copiedResult[0] || {} 134 | 135 | return new SuccessRes({ 136 | count, 137 | use: parseInt(obj.copiedCount, 10) || 0, 138 | }) 139 | } 140 | 141 | /** 142 | * 按月统计创建和使用量 143 | */ 144 | async function getMonthlyCount() { 145 | // 一年的时间范围 146 | const d = new Date() 147 | const startTime = new Date(d.getTime() - 365 * 24 * 60 * 60 * 1000) // 一年之前 148 | const endTime = d 149 | 150 | // 获取每月创建的数量 151 | const createdResult = await getCreatedCountMonthlyService(startTime, endTime) 152 | 153 | // 获取使用的数量 154 | const copiedCountResult = await getCopiedCountMonthlyService(startTime, endTime) 155 | 156 | // 格式化数据 157 | const resDataObj = {} 158 | createdResult.forEach(item => { 159 | const { month, count = 0 } = item || {} 160 | if (!month) return 161 | if (resDataObj[month] == null) resDataObj[month] = {} 162 | 163 | const data = resDataObj[month] 164 | data.count = count 165 | if (data.use == null) data.use = 0 166 | }) 167 | copiedCountResult.forEach(item => { 168 | const { month, copiedCount = 0 } = item || {} 169 | if (!month) return 170 | if (resDataObj[month] == null) resDataObj[month] = {} 171 | 172 | const data = resDataObj[month] 173 | data.use = parseInt(copiedCount, 10) 174 | if (data.count == null) data.count = 0 175 | }) 176 | const monthKeys = Object.keys(resDataObj) 177 | const resData = monthKeys.map(month => { 178 | return { 179 | month, 180 | data: resDataObj[month], 181 | } 182 | }) 183 | 184 | return new SuccessRes(resData) 185 | } 186 | 187 | module.exports = { 188 | getTemplateList, 189 | updateIsPublic, 190 | updateIsHot, 191 | updateIsNew, 192 | updateOrderIndex, 193 | getCount, 194 | getMonthlyCount, 195 | } 196 | --------------------------------------------------------------------------------