├── src ├── index.d.ts ├── config │ ├── config.ts │ └── init.sql ├── routes │ ├── index.ts │ ├── auth.ts │ ├── race.ts │ ├── permission.ts │ ├── record.ts │ ├── role.ts │ └── user.ts ├── middlewares │ ├── login-check.ts │ └── auth-check.ts ├── app.ts ├── utils │ └── qiniu.ts └── db │ └── model.ts ├── .gitignore ├── README.md ├── .eslintrc.js ├── package.json └── tsconfig.json /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | interface Request { 3 | user: { 4 | permissions: any[], 5 | role: any, 6 | account: string, 7 | identity: 'student' | 'teacher' 8 | [key: string]: any; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/config/config.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'sequelize'; 2 | 3 | // 可以自己定义 4 | export const tokenKey = 'token-ncu_university-competition_server'; 5 | export const cookieKey = 'cookie-ncu_university-competition_server'; 6 | 7 | export const DataBaseConfig: Options = { 8 | dialect: 'mysql', // 数据库使用mysql 9 | port: 3306, // 数据库服务器端口 10 | host: '', // 数据库服务器ip 11 | database: '', // 数据库名 12 | username: '', // 用户名 13 | password: '', // 用户密码 14 | }; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 竞赛信息管理系统后台 2 | 3 | [前端项目地址](https://github.com/1446445040/competition-system) 4 | 5 | 后端项目基于 Node.js 开发,主要使用 TypeScript 编写,使用 Express 框架构建 Web 服务器,使用 [Sequelize](https://www.sequelize.com.cn/) 框架操作 MySQL 数据库。 6 | 7 | 用都用了,点个星星呗。 8 | 9 | # 运行方法 10 | 11 | 请预先安装 Node.js 环境,并在项目根目录下运行以下命令: 12 | 13 | ```shell 14 | npm install --registry https://registry.npm.taobao.org/ # 使用淘宝镜像源安装依赖包 15 | npm run dev # 启动项目 16 | ``` 17 | 18 | `src/config/config.ts`为配置文件,请填写相关数据库配置,否则无法正常连接。 19 | 20 | `src/config/init.sql`为数据库基础数据,sql 文件为 Navicat 导出。请使用该 sql 文件在数据库中初始化数据,否则可能无法正常登陆,大佬自便。 21 | 22 | 项目默认运行在`3000`端口,如果手动更改端口,请与前端配置保持一致。 23 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import User from './user'; 3 | import Race from './race'; 4 | import Record from './record'; 5 | import Auth from './auth'; 6 | import Permission from './permission'; 7 | import Role from './role'; 8 | import loginChecker from '@/middlewares/login-check'; 9 | import authChecker from '@/middlewares/auth-check'; 10 | 11 | const router = Router(); 12 | 13 | router.use(Auth); 14 | router.use(loginChecker); 15 | router.use(authChecker); 16 | router.use(User); 17 | router.use(Race); 18 | router.use(Record); 19 | router.use(Permission); 20 | router.use(Role); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true, 5 | }, 6 | globals: { 7 | Atomics: 'readonly', 8 | SharedArrayBuffer: 'readonly', 9 | }, 10 | parser: '@typescript-eslint/parser', 11 | parserOptions: { 12 | ecmaVersion: 11, 13 | sourceType: 'module', 14 | }, 15 | plugins: [ 16 | '@typescript-eslint', 17 | ], 18 | extends: [ 19 | 'standard', 20 | 'plugin:@typescript-eslint/recommended', 21 | ], 22 | rules: { 23 | camelcase: 'off', 24 | semi: ['error', 'always'], 25 | '@typescript-eslint/ban-ts-comment': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | 'no-async-promise-executor': 'off', 28 | '@typescript-eslint/ban-types': 'off', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | 'comma-dangle': ['error', 'always-multiline'], // 末尾逗号 31 | '@typescript-eslint/triple-slash-reference': 'off', 32 | 'space-before-function-paren': ['error', { 33 | anonymous: 'never', 34 | named: 'never', 35 | asyncArrow: 'always', // async箭头函数 36 | }], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/middlewares/login-check.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { verify } from 'jsonwebtoken'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | import { tokenKey } from '../config/config'; 5 | import { getUserModel, Roles } from '../db/model'; 6 | 7 | export default function(req: Request, res: Response, next: NextFunction) { 8 | const token = req.signedCookies.uid; 9 | if (typeof token !== 'string') { 10 | return res.json({ 11 | code: 403, 12 | msg: '拒绝访问', 13 | }); 14 | } 15 | verify(token, tokenKey, async function(err, payload: any) { 16 | if (err) { 17 | return res.json({ 18 | code: 403, 19 | msg: '拒绝访问', 20 | }); 21 | } 22 | const { exp, identity, account } = payload; 23 | if (dayjs().isAfter(exp)) { 24 | return res.json({ 25 | code: 403, 26 | msg: '请重新登陆', 27 | }); 28 | } 29 | const user = await getUserModel(identity).findByPk(account); 30 | const role_id = user?.getDataValue('role_id'); 31 | const role = await Roles.findByPk(role_id); 32 | // @ts-ignore 33 | const permissions = (await role?.getPermissions()) || []; 34 | 35 | // 将user挂载到请求对象上 36 | req.user = { 37 | account, 38 | identity, 39 | role, 40 | permissions: permissions.reduce((res, v) => { 41 | // 拼接成"user:add"这样的字符串 42 | return res.concat(`${v.type}:${v.action}`); 43 | }, []), 44 | }; 45 | next(); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import 'module-alias/register'; 3 | import 'express-async-errors'; 4 | import express, { Response, Request, NextFunction, json, urlencoded } from 'express'; 5 | import { ValidationError } from 'sequelize'; 6 | import { cookieKey } from '@/config/config'; 7 | import consola from 'consola'; 8 | import morgan from 'morgan'; 9 | import cookieParser from 'cookie-parser'; 10 | import RateLimit from 'express-rate-limit'; 11 | import Router from '@/routes'; 12 | 13 | const app = express(); 14 | 15 | // 中间件 16 | app.use(morgan('tiny')); // 请求日志 17 | app.use(cookieParser(cookieKey)); 18 | app.use(json()); 19 | app.use(urlencoded({ extended: true })); 20 | app.use(RateLimit({ 21 | windowMs: 1000, 22 | max: 5, 23 | handler(req, res) { 24 | res.json({ 25 | code: 429, 26 | msg: '请求太频繁,歇会吧~', 27 | }); 28 | }, 29 | })); 30 | 31 | // 路由 32 | app.use('/api', Router); 33 | 34 | // @ts-ignore 错误处理 35 | app.use((e, req: Request, res: Response, next: NextFunction) => { 36 | if (e instanceof ValidationError) { 37 | res.json({ 38 | code: 400, 39 | msg: e.errors.map(item => item.message).join('--'), 40 | }); 41 | } else { 42 | res.json({ 43 | code: 500, 44 | msg: e.message, 45 | }); 46 | } 47 | next(e); 48 | }); 49 | 50 | app.listen(3000, function() { 51 | consola.ready({ 52 | message: `Server is listening on http://localhost:${3000}`, 53 | badge: true, 54 | }); 55 | }); 56 | 57 | // 捕获可能遗漏的错误,防止程序崩溃 58 | process.on('uncaughtException', function(e) { 59 | consola.error(e); 60 | }); 61 | 62 | process.on('unhandledRejection', function(e) { 63 | consola.error(e); 64 | }); 65 | -------------------------------------------------------------------------------- /src/middlewares/auth-check.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | /** 4 | * 生成权限校验函数 5 | * @param {string} type 权限类别 6 | * @returns {function} 7 | */ 8 | export function check(type: string) { 9 | return function(req: Request) { 10 | return req.user.permissions.some(v => v === type); 11 | }; 12 | } 13 | 14 | type Checkers = Record boolean> 15 | const strategy: Checkers = { 16 | // 用户管理 17 | '/user/add': check('user:add'), 18 | '/user/delete': check('user:delete'), 19 | '/user/reset': check('user:update'), 20 | '/user/list': check('user:query'), 21 | // 赛事管理 22 | '/race/add': check('race:add'), 23 | '/race/delete': check('race:delete'), 24 | '/race/list': check('race:query'), 25 | '/race/update': check('race:update'), 26 | // 参赛记录管理 27 | '/record/add': check('record:add'), 28 | '/record/delete': check('record:delete'), 29 | '/record/list': check('record:query'), 30 | // 权限管理 31 | '/permission/list': check('permission:query'), 32 | '/permission/add': check('permission:add'), 33 | '/permission/delete': check('permission:delete'), 34 | '/permission/update': check('permission:update'), 35 | // 角色管理 36 | '/role/list': check('role:query'), 37 | '/role/add': check('role:add'), 38 | '/role/delete': check('role:delete'), 39 | '/role/update': check('role:update'), 40 | '/role/grant': check('role:update'), 41 | }; 42 | 43 | /** 44 | * 前置校验权限 45 | */ 46 | export default function(req: Request, res: Response, next: NextFunction) { 47 | const checker = strategy[req.path]; 48 | 49 | if (!checker || checker(req)) return next(); 50 | 51 | res.json({ 52 | code: 401, 53 | msg: '暂无权限', 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "system-server", 3 | "version": "1.0.0", 4 | "description": "竞赛管理系统服务端", 5 | "scripts": { 6 | "dev": "ts-node-dev src/app.ts", 7 | "start": "ts-node src/app.ts", 8 | "lint": "eslint --fix --ext .ts" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "consola": "^2.15.3", 16 | "cookie-parser": "^1.4.5", 17 | "dayjs": "^1.10.4", 18 | "express": "^4.17.1", 19 | "express-async-errors": "^3.1.1", 20 | "express-rate-limit": "^5.2.6", 21 | "jsonwebtoken": "^8.5.1", 22 | "lodash": "^4.17.21", 23 | "mysql2": "^2.2.5", 24 | "qiniu": "^7.3.2", 25 | "sequelize": "^6.6.2", 26 | "svg-captcha": "^1.4.0" 27 | }, 28 | "devDependencies": { 29 | "@types/bcryptjs": "^2.4.2", 30 | "@types/cookie-parser": "^1.4.2", 31 | "@types/express": "^4.17.11", 32 | "@types/express-rate-limit": "^5.1.1", 33 | "@types/jsonwebtoken": "^8.5.1", 34 | "@types/lodash": "^4.14.168", 35 | "@types/morgan": "^1.9.2", 36 | "@types/qiniu": "^7.0.1", 37 | "@typescript-eslint/eslint-plugin": "^4.22.0", 38 | "@typescript-eslint/parser": "^4.22.0", 39 | "eslint": "^7.24.0", 40 | "eslint-config-standard": "^16.0.2", 41 | "eslint-plugin-import": "^2.22.1", 42 | "eslint-plugin-node": "^11.1.0", 43 | "eslint-plugin-promise": "^4.3.1", 44 | "eslint-plugin-standard": "^4.1.0", 45 | "module-alias": "^2.2.2", 46 | "morgan": "^1.10.0", 47 | "ts-node": "^9.1.1", 48 | "ts-node-dev": "^1.1.6", 49 | "tsc": "^1.20150623.0", 50 | "typescript": "^4.2.4" 51 | }, 52 | "_moduleAliases": { 53 | "@": "src" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { compareSync } from 'bcryptjs'; 3 | import { getUserModel } from '@/db/model'; 4 | import { sign } from 'jsonwebtoken'; 5 | import { tokenKey } from '@/config/config'; 6 | import svg from 'svg-captcha'; 7 | import dayjs from 'dayjs'; 8 | 9 | const router = Router(); 10 | 11 | router.post('/auth/login', async (req: Request, res: Response) => { 12 | const sysCode: string = req.signedCookies.code; 13 | const { account, password, identity, code } = req.body; 14 | if (!account || !password || !identity || !code) { 15 | return res.json({ 16 | code: 400, 17 | msg: '参数错误', 18 | }); 19 | } 20 | if (!sysCode || code.toLowerCase() !== sysCode.toLowerCase()) { 21 | return res.json({ 22 | code: 3, 23 | msg: '验证码有误', 24 | }); 25 | } 26 | const UserModel = getUserModel(identity); 27 | const user = await UserModel.findByPk(account); 28 | if (!user) { 29 | return res.json({ 30 | code: 1, 31 | msg: '用户不存在', 32 | }); 33 | } 34 | const passwordHash = user.getDataValue('password'); 35 | if (!compareSync(password, passwordHash)) { 36 | return res.json({ 37 | code: 2, 38 | msg: '密码错误', 39 | }); 40 | } 41 | const exp = dayjs().add(7, 'day'); 42 | res.cookie('uid', sign({ 43 | account, 44 | identity, 45 | exp: exp.valueOf(), 46 | }, tokenKey), { 47 | expires: exp.toDate(), 48 | signed: true, 49 | }); 50 | res.json({ 51 | code: 200, 52 | msg: '登陆成功', 53 | }); 54 | }); 55 | 56 | router.get('/auth/code', async (req: Request, res: Response) => { 57 | const code = svg.create({ noise: 2, width: 100 }); 58 | res.set('cache-control', 'no-cache'); 59 | res.cookie('code', code.text, { httpOnly: true, signed: true, maxAge: 5 * 60 * 1000 }); 60 | res.json({ 61 | code: 200, 62 | msg: '获取成功', 63 | data: code.data, 64 | }); 65 | }); 66 | 67 | export default router; 68 | -------------------------------------------------------------------------------- /src/routes/race.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, Router } from 'express'; 2 | import { toNumber } from 'lodash'; 3 | import { Races, likeQuery } from '@/db/model'; 4 | import { Op } from 'sequelize'; 5 | 6 | const router = Router(); 7 | 8 | router.get('/race/list', async (req: Request, res: Response) => { 9 | const { 10 | limit, 11 | offset, 12 | title, 13 | location, 14 | sponsor, 15 | date, 16 | ...query 17 | } = req.query; 18 | 19 | Object.assign(query, likeQuery({ title, location, sponsor })); 20 | if (typeof date === 'string') { 21 | query.date = { [Op.between]: date.split('~') }; 22 | } 23 | 24 | const { rows, count } = await Races.findAndCountAll({ 25 | where: query, 26 | limit: toNumber(limit) || undefined, 27 | offset: toNumber(limit) * (toNumber(offset) - 1) || undefined, 28 | }); 29 | res.json({ 30 | code: 200, 31 | msg: '查询成功', 32 | count, 33 | data: rows.map(item => item.toJSON()), 34 | }); 35 | }); 36 | 37 | router.post('/race/add', async (req: Request, res: Response) => { 38 | const data = req.body; 39 | if (!data) { 40 | return res400(res); 41 | } 42 | await Races.create(data); 43 | res.json({ 44 | code: 200, 45 | msg: '添加成功', 46 | }); 47 | }); 48 | 49 | router.delete('/race/delete', async (req: Request, res: Response) => { 50 | const data = req.body; 51 | if (!Array.isArray(data)) { 52 | return res400(res); 53 | } 54 | await Races.destroy({ 55 | where: { race_id: data }, 56 | }); 57 | res.json({ 58 | code: 200, 59 | msg: '删除成功', 60 | }); 61 | }); 62 | 63 | router.put('/race/update', async (req: Request, res: Response) => { 64 | const data = req.body; 65 | const { race_id, ...otherData } = data; 66 | if (!race_id) { 67 | return res400(res); 68 | } 69 | await Races.update(otherData, { 70 | where: { [Races.primaryKeyAttribute]: race_id }, 71 | }); 72 | res.json({ 73 | code: 200, 74 | msg: '修改成功', 75 | }); 76 | }); 77 | 78 | export default router; 79 | 80 | function res400(res: Response) { 81 | return res.json({ 82 | code: 400, 83 | msg: '参数有误', 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /src/utils/qiniu.ts: -------------------------------------------------------------------------------- 1 | import qiniu from 'qiniu'; 2 | import { bucket, accessKey, secretKey, domain } from '../config/config'; 3 | 4 | // 鉴权对象 5 | const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); 6 | 7 | const config = new qiniu.conf.Config(); 8 | const bucketManager = new qiniu.rs.BucketManager(mac, config); 9 | const cdnManager = new qiniu.cdn.CdnManager(mac); 10 | 11 | /** 12 | * 获取上传token 13 | * @param name 可选,用于指定是否覆盖上传 14 | */ 15 | export const getToken = function(name = ''): string { 16 | const putPolicy = new qiniu.rs.PutPolicy({ 17 | scope: name ? `${bucket}:${name}` : bucket, 18 | expires: 60, 19 | }); 20 | return putPolicy.uploadToken(mac); 21 | }; 22 | 23 | /** 24 | * 获取下载链接 25 | * @param filename 文件名 26 | */ 27 | export const getFileUrl = function(filename: string): string { 28 | // deadline需要新建,否则每次返回的url是一样的 29 | const deadline = Math.trunc(Date.now() / 1000) + 60; // 1分钟过期 30 | return bucketManager.privateDownloadUrl(domain, filename, deadline); 31 | }; 32 | 33 | /** 34 | * cdn 文件刷新 35 | * @param name 要刷新的文件名 36 | */ 37 | export const refreshUrl = function(name: string): Promise { 38 | return new Promise((resolve, reject) => { 39 | cdnManager.refreshUrls([`${domain}/${name}`], function(err, body) { 40 | if (err) { 41 | return reject(err); 42 | } 43 | 44 | if (body.code !== 200) { 45 | reject(body.error); 46 | } else { 47 | resolve(body); 48 | } 49 | }); 50 | }); 51 | }; 52 | 53 | interface FileInfo { 54 | fsize: number; 55 | hash: string; 56 | md5: string; 57 | mimeType: string; 58 | putTime: number; 59 | type: number; 60 | } 61 | /** 62 | * 获取文件信息 63 | * @param name 文件名 64 | */ 65 | export const getFileInfo = function(name: string): Promise { 66 | return new Promise((resolve, reject) => { 67 | bucketManager.stat( 68 | bucket, 69 | name, 70 | function(err, data, { statusCode }) { 71 | if (err) { 72 | reject(err); 73 | } else if (statusCode !== 200) { 74 | reject(new Error(data.error)); 75 | } else { 76 | resolve(data); 77 | } 78 | }, 79 | ); 80 | }); 81 | }; 82 | 83 | /** 84 | * 删除文件 85 | * @param names 文件名 86 | */ 87 | export const deleteFile = function(names: string[]) { 88 | const deleteOptions = names.map(name => { 89 | return qiniu.rs.deleteOp(bucket, name); 90 | }); 91 | return new Promise((resolve, reject) => { 92 | bucketManager.batch( 93 | deleteOptions, 94 | function(err, data) { 95 | if (err) { 96 | reject(err); 97 | } else { 98 | resolve(data); 99 | } 100 | }, 101 | ); 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /src/routes/permission.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { toNumber, pick } from 'lodash'; 3 | import { likeQuery, Permissions, sequelize } from '@/db/model'; 4 | 5 | const router = Router(); 6 | 7 | router.get('/permission/list', async (req: Request, res: Response) => { 8 | const { 9 | limit, 10 | offset, 11 | label, 12 | ...query 13 | } = req.query; 14 | Object.assign(query, likeQuery({ label })); 15 | 16 | const { rows, count } = await Permissions.findAndCountAll({ 17 | where: query, 18 | limit: toNumber(limit) || undefined, 19 | offset: toNumber(limit) * (toNumber(offset) - 1) || undefined, 20 | }); 21 | res.json({ 22 | code: 200, 23 | msg: '查询成功', 24 | count, 25 | data: rows.map(item => item.toJSON()), 26 | }); 27 | }); 28 | 29 | router.post('/permission/add', async (req: Request, res: Response) => { 30 | const data = req.body; 31 | if (!data) { 32 | return res.json({ 33 | code: 400, 34 | msg: '参数有误', 35 | }); 36 | } 37 | const isExist = await Permissions.findOne({ where: pick(data, ['action', 'type']) }); 38 | if (isExist) { 39 | return res.json({ 40 | code: 400, 41 | msg: '权限已存在', 42 | }); 43 | } 44 | await Permissions.create(data); 45 | res.json({ 46 | code: 200, 47 | msg: '添加成功', 48 | }); 49 | }); 50 | 51 | router.delete('/permission/delete', async (req: Request, res: Response) => { 52 | const data = req.body; 53 | if (!Array.isArray(data)) { 54 | return res.json({ 55 | code: 400, 56 | msg: '参数有误', 57 | }); 58 | } 59 | await sequelize.transaction(async transaction => { 60 | for (const id of data) { 61 | const permission = await Permissions.findByPk(id); 62 | // @ts-ignore 63 | const num = await permission.countRoles(); 64 | if (num === 0) { 65 | await Permissions.destroy({ where: { id: data }, transaction }); 66 | } else { 67 | throw new Error(`id为${id}的权限被${num}个角色引用,不能删除`); 68 | } 69 | } 70 | }); 71 | res.json({ 72 | code: 200, 73 | msg: '删除成功', 74 | }); 75 | }); 76 | 77 | router.post('/permission/update', async (req: Request, res: Response) => { 78 | const data = req.body; 79 | const { id, ...otherData } = data; 80 | if (!id) { 81 | return res.json({ 82 | code: 400, 83 | msg: '参数有误', 84 | }); 85 | } 86 | const isExist = await Permissions.findOne({ where: pick(otherData, ['action', 'type']) }); 87 | if (!isExist) { 88 | return res.json({ 89 | code: 400, 90 | msg: '权限不存在', 91 | }); 92 | } 93 | await Permissions.update(otherData, { where: { id } }); 94 | res.json({ 95 | code: 200, 96 | msg: '修改成功', 97 | }); 98 | }); 99 | 100 | export default router; 101 | -------------------------------------------------------------------------------- /src/routes/record.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { Records, Races, Students, Teachers, likeQuery } from '@/db/model'; 3 | import { pick, toNumber } from 'lodash'; 4 | import { check } from '@/middlewares/auth-check'; 5 | 6 | const router = Router(); 7 | 8 | router.get('/record/list', async (req: Request, res: Response) => { 9 | const { limit, offset, tname, sname, title, score, ...recordQuery } = req.query; 10 | Object.assign(recordQuery, likeQuery({ 11 | score, 12 | '$race.title$': title, 13 | '$teacher.name$': tname, 14 | '$student.name$': sname, 15 | })); 16 | const { rows, count } = await Records.findAndCountAll({ 17 | where: recordQuery, 18 | limit: toNumber(limit) || undefined, 19 | offset: toNumber(limit) * (toNumber(offset) - 1) || undefined, 20 | include: [ 21 | { model: Students, attributes: [['name', 'sname']] }, 22 | { model: Teachers, attributes: [['name', 'tname']] }, 23 | { model: Races, attributes: ['title'] }, 24 | ], 25 | }); 26 | res.json({ 27 | code: 200, 28 | msg: '查询成功', 29 | count, 30 | data: rows.map(item => { 31 | const { student, race, teacher, ...data }: Record = item.toJSON(); 32 | // 二级属性平铺 33 | return Object.assign(data, student, race, teacher); 34 | }), 35 | }); 36 | }); 37 | 38 | router.delete('/record/delete', async (req: Request, res: Response) => { 39 | const data = req.body; 40 | if (!Array.isArray(data)) { 41 | return res400(res); 42 | } 43 | await Records.destroy({ where: { record_id: data } }); 44 | res.json({ 45 | code: 200, 46 | msg: '删除成功', 47 | }); 48 | }); 49 | 50 | router.post('/record/add', async (req: Request, res: Response) => { 51 | const data = req.body; 52 | if (!data) return res400(res); 53 | const msg = await validateRecord(data); 54 | if (msg) return res.json({ code: 400, msg }); 55 | 56 | await Records.create(data); 57 | res.json({ 58 | code: 200, 59 | msg: '创建成功', 60 | }); 61 | }); 62 | 63 | const checkRecordUpdate = check('record:update'); 64 | router.patch('/record/update', async (req: Request, res: Response) => { 65 | const { record_id, ...data } = req.body; 66 | const update = async data => await Records.update(data, { where: { record_id } }); 67 | const record = await Records.findByPk(record_id); 68 | // @ts-ignore 学生自己 69 | const student = await record.getStudent(); 70 | 71 | if (!record || !student) { 72 | return res.json({ code: 404, msg: '记录不存在' }); 73 | } 74 | 75 | let isUpdateSuccess = false; 76 | // 有权限直接修改 77 | if (checkRecordUpdate(req)) { 78 | await update(data); 79 | isUpdateSuccess = true; 80 | } else if ( 81 | // 判断是否是学生,学生可以改自己的录入成绩 82 | student.getDataValue('sid') === req.user.account && 83 | req.user.identity === 'student' 84 | ) { 85 | await update(pick(data, ['score'])); 86 | isUpdateSuccess = true; 87 | } 88 | 89 | if (isUpdateSuccess) { 90 | return res.json({ 91 | code: 200, 92 | msg: '修改成功', 93 | }); 94 | } 95 | res.json({ 96 | code: 401, 97 | msg: '暂无权限', 98 | }); 99 | }); 100 | 101 | export default router; 102 | 103 | function res400(res: Response) { 104 | return res.json({ 105 | code: 400, 106 | msg: '参数有误', 107 | }); 108 | } 109 | 110 | async function validateRecord(data : any = {}) { 111 | const { race_id, sid, tid } = data; 112 | if (!race_id || !sid) { 113 | return '参数有误'; 114 | } 115 | 116 | // 检查是否已存在相同记录 117 | const record = await Records.findOne({ where: { race_id, sid } }); 118 | if (record) { 119 | return '请勿重复报名'; 120 | } 121 | const race = await Races.findByPk(race_id); 122 | // 检查比赛是否存在 123 | if (!race) { 124 | return '比赛不存在'; 125 | } 126 | 127 | const student = await Students.findByPk(sid); 128 | if (!student) { 129 | return '学生信息不存在'; 130 | } 131 | if (tid && !await Teachers.findByPk(tid)) { 132 | return '教师信息不存在'; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/routes/role.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { toNumber } from 'lodash'; 3 | import { 4 | likeQuery, 5 | getUserModel, 6 | Roles, 7 | Permissions, 8 | sequelize, 9 | } from '@/db/model'; 10 | 11 | const router = Router(); 12 | 13 | router.get('/role/list', async (req: Request, res: Response) => { 14 | const { 15 | limit, 16 | offset, 17 | label, 18 | description, 19 | ...query 20 | } = req.query; 21 | Object.assign(query, likeQuery({ label, description })); 22 | 23 | const { rows, count } = await Roles.findAndCountAll({ 24 | where: query, 25 | limit: toNumber(limit) || undefined, 26 | offset: toNumber(limit) * (toNumber(offset) - 1) || undefined, 27 | distinct: true, // 防止重复计数 28 | include: { 29 | model: Permissions, 30 | attributes: { 31 | include: ['action', 'id', 'label'], 32 | }, 33 | }, 34 | }); 35 | 36 | res.json({ 37 | code: 200, 38 | msg: '查询成功', 39 | count, 40 | data: rows.map(item => item.toJSON()), 41 | }); 42 | }); 43 | 44 | router.post('/role/add', async (req: Request, res: Response) => { 45 | const { permissions = [], ...data } = req.body; 46 | if (!data) { 47 | return res.json({ 48 | code: 400, 49 | msg: '参数有误', 50 | }); 51 | } 52 | 53 | await sequelize.transaction(async transaction => { 54 | const role = await Roles.create(data, { transaction }); 55 | const permission_models = await Permissions.findAll({ 56 | where: { id: permissions }, 57 | transaction, 58 | }); 59 | // @ts-ignore 60 | role.setPermissions(permission_models); 61 | }); 62 | 63 | res.json({ 64 | code: 200, 65 | msg: '添加成功', 66 | }); 67 | }); 68 | 69 | router.delete('/role/delete', async (req: Request, res: Response) => { 70 | const data = req.body; 71 | if (!Array.isArray(data)) { 72 | return res.json({ 73 | code: 400, 74 | msg: '参数有误', 75 | }); 76 | } 77 | await sequelize.transaction(async transaction => { 78 | for (const id of data) { 79 | const role = await Roles.findByPk(id); 80 | const [stu_num, tea_num] = [ 81 | // @ts-ignore 82 | await role.countStudents(), 83 | // @ts-ignore 84 | await role.countTeachers(), 85 | ]; 86 | if (stu_num === 0 && tea_num === 0) { 87 | await Roles.destroy({ where: { id: data }, transaction }); 88 | } else { 89 | throw new Error(`角色${id}包含引用${stu_num + tea_num}个,不能删除`); 90 | } 91 | } 92 | }); 93 | 94 | res.json({ 95 | code: 200, 96 | msg: '删除成功', 97 | }); 98 | }); 99 | 100 | router.post('/role/update', async (req: Request, res: Response) => { 101 | const data = req.body; 102 | const { id, permissions, ...otherData } = data; 103 | if (!id) { 104 | return res.json({ 105 | code: 400, 106 | msg: '参数有误', 107 | }); 108 | } 109 | 110 | await sequelize.transaction(async t => { 111 | const options = { where: { id }, transaction: t }; 112 | await Roles.update(otherData, options); 113 | const role = await Roles.findOne(options); 114 | const permission_models = await Permissions.findAll({ 115 | where: { id: permissions }, 116 | }); 117 | // @ts-ignore 118 | role.setPermissions(permission_models); 119 | }); 120 | 121 | res.json({ 122 | code: 200, 123 | msg: '修改成功', 124 | }); 125 | }); 126 | 127 | router.post('/role/grant', async (req: Request, res: Response) => { 128 | const { type, account, role_id } = req.body; 129 | if (!type || !account || !role_id) { 130 | return res.json({ 131 | code: 400, 132 | msg: '参数有误', 133 | }); 134 | } 135 | const UserModal = getUserModel(type); 136 | const data = await UserModal.update({ role_id }, { 137 | where: { 138 | [UserModal.primaryKeyAttribute]: account, 139 | }, 140 | }); 141 | console.log(data); 142 | 143 | res.json({ 144 | code: 200, 145 | msg: '操作成功', 146 | }); 147 | }); 148 | 149 | export default router; 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | /* Strict Type-Checking Options */ 25 | "strict": true, /* Enable all strict type-checking options. */ 26 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 27 | // "strictNullChecks": true, /* Enable strict null checks. */ 28 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 29 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 30 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 31 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 32 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 33 | /* Additional Checks */ 34 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 35 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 36 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 37 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | "paths": { 42 | "@/*": [ 43 | "src/*" 44 | ] 45 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | /* Advanced Options */ 62 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 63 | }, 64 | } -------------------------------------------------------------------------------- /src/db/model.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { genSaltSync, hashSync } from 'bcryptjs'; 3 | import { DataBaseConfig } from '@/config/config'; 4 | import { 5 | Sequelize, 6 | DataTypes, 7 | Model, 8 | ModelSetterOptions, 9 | ModelGetterOptions, 10 | Op, 11 | } from 'sequelize'; 12 | 13 | export const sequelize = new Sequelize({ 14 | ...DataBaseConfig, 15 | logging: false, 16 | define: { 17 | charset: 'utf8', 18 | underscored: true, // 字段以下划线(_)来分割(默认是驼峰命名风格) 19 | timestamps: false, 20 | createdAt: 'create_time', 21 | updatedAt: 'update_time', 22 | getterMethods: genGetter( 23 | ['create_time', 'update_time', 'date'], 24 | time => { 25 | return dayjs(time).format('YYYY-MM-DD HH:mm:ss'); 26 | }, 27 | ), 28 | }, 29 | }); 30 | 31 | export const Students = sequelize.define('student', { 32 | sid: { type: DataTypes.STRING, allowNull: false, primaryKey: true }, 33 | name: { type: DataTypes.STRING, allowNull: false }, 34 | password: { type: DataTypes.STRING, allowNull: false, defaultValue: '123456' }, 35 | sex: { type: DataTypes.INTEGER, allowNull: false }, // 0 女, 1 男 36 | grade: { type: DataTypes.INTEGER, allowNull: false }, 37 | class: { type: DataTypes.STRING, allowNull: false }, 38 | }, { 39 | timestamps: true, 40 | setterMethods: { 41 | ...trim(['sid', 'name', 'class']), 42 | password: setPassword, 43 | }, 44 | }); 45 | 46 | export const Teachers = sequelize.define('teacher', { 47 | tid: { type: DataTypes.STRING, allowNull: false, primaryKey: true }, 48 | name: { type: DataTypes.STRING, allowNull: false }, 49 | password: { type: DataTypes.STRING, allowNull: false, defaultValue: '123456' }, 50 | rank: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, // 职称 0: 其他 51 | description: { type: DataTypes.STRING }, 52 | }, { 53 | timestamps: true, 54 | setterMethods: { 55 | ...trim(['tid', 'name', 'description']), 56 | password: setPassword, 57 | }, 58 | }); 59 | 60 | export const Races = sequelize.define('race', { 61 | race_id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, 62 | title: { type: DataTypes.STRING, allowNull: false }, 63 | sponsor: { type: DataTypes.STRING, allowNull: false }, 64 | type: { type: DataTypes.STRING, allowNull: false }, 65 | level: { type: DataTypes.INTEGER, allowNull: false }, 66 | location: { type: DataTypes.STRING, allowNull: false }, 67 | date: { type: DataTypes.DATE, allowNull: false }, 68 | description: { type: DataTypes.STRING }, 69 | }, { 70 | timestamps: true, 71 | setterMethods: { 72 | ...trim(['title', 'sponsor', 'location', 'description']), 73 | }, 74 | }); 75 | 76 | export const Records = sequelize.define('record', { 77 | record_id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, 78 | status: { type: DataTypes.INTEGER, defaultValue: 0 }, 79 | score: { type: DataTypes.STRING }, 80 | description: { type: DataTypes.STRING, defaultValue: '' }, 81 | }, { 82 | timestamps: true, 83 | setterMethods: { 84 | ...trim(['score', 'description']), 85 | }, 86 | }); 87 | 88 | Records.belongsTo(Students, { foreignKey: 'sid' }); 89 | Students.hasMany(Records, { foreignKey: 'sid' }); 90 | Records.belongsTo(Teachers, { foreignKey: 'tid' }); 91 | Teachers.hasMany(Records, { foreignKey: 'tid' }); 92 | Records.belongsTo(Races, { foreignKey: 'race_id' }); 93 | Races.hasMany(Records, { foreignKey: 'race_id' }); 94 | 95 | export const Roles = sequelize.define('role', { 96 | label: { type: DataTypes.STRING, allowNull: false, unique: true }, 97 | description: { type: DataTypes.STRING }, 98 | }, { 99 | setterMethods: { 100 | ...trim(['label', 'description']), 101 | }, 102 | }); 103 | 104 | export const Permissions = sequelize.define('permission', { 105 | label: { type: DataTypes.STRING, allowNull: false, unique: true }, 106 | action: { 107 | allowNull: false, 108 | type: DataTypes.ENUM('add', 'delete', 'update', 'query', 'import', 'export'), 109 | }, 110 | type: { 111 | allowNull: false, 112 | type: DataTypes.ENUM('user', 'role', 'race', 'record', 'permission'), 113 | }, 114 | }, { 115 | setterMethods: { 116 | ...trim(['label', 'description']), 117 | }, 118 | }); 119 | 120 | // 学生、教师与角色的n:1映射关系 121 | Roles.hasMany(Students, { foreignKey: 'role_id' }); 122 | Students.belongsTo(Roles, { foreignKey: 'role_id' }); 123 | Roles.hasMany(Teachers, { foreignKey: 'role_id' }); 124 | Teachers.belongsTo(Roles, { foreignKey: 'role_id' }); 125 | 126 | // 权限与角色之间的m:n多对多映射关系 127 | Permissions.belongsToMany(Roles, { 128 | through: 'role_permission', 129 | }); 130 | Roles.belongsToMany(Permissions, { 131 | through: 'role_permission', 132 | }); 133 | 134 | export function getUserModel(type: string) { 135 | return type === 'teacher' ? Teachers : Students; 136 | } 137 | 138 | /** 139 | * 批量生成setter 140 | * @param keys 需要生成setter的字段 141 | * @param convert 定义数据转化方式 142 | */ 143 | function genSetter( 144 | keys: string[], 145 | convert: (value: any) => any, 146 | ) { 147 | const result: ModelSetterOptions = {}; 148 | for (const key of keys) { 149 | result[key] = function(value: string) { 150 | this.setDataValue(key, convert(value)); 151 | }; 152 | } 153 | return result; 154 | } 155 | 156 | /** 157 | * 批量生成getter 158 | * @param keys 需要生成getter的字段 159 | * @param convert 定义数据转化方式 160 | */ 161 | function genGetter( 162 | keys: string[], 163 | convert: (value: any) => any, 164 | ) { 165 | const result: ModelGetterOptions = {}; 166 | for (const key of keys) { 167 | result[key] = function() { 168 | return convert(this.getDataValue(key)); 169 | }; 170 | } 171 | return result; 172 | } 173 | 174 | /** 175 | * 密码设置器, 用于拦截密码设置操作, 计算哈希密码存入数据库 176 | * @param{string} value 密码 177 | */ 178 | function setPassword(this: Model, value: string) { 179 | this.setDataValue('password', hashSync(value.trim(), genSaltSync(10))); 180 | } 181 | 182 | /** 183 | * 用于对字符串进行trim操作 184 | * @param{string[]} keys 185 | */ 186 | function trim(keys: string[]) { 187 | return genSetter(keys, value => value?.trim()); 188 | } 189 | 190 | /** 191 | * 构造`%{query}%`查询, 空字段将被过滤 192 | * @param query 193 | */ 194 | export function likeQuery(query: object) { 195 | const result: Record = {}; 196 | for (const [key, value] of Object.entries(query)) { 197 | if (!value) continue; 198 | result[key] = { [Op.substring]: value }; 199 | } 200 | return result; 201 | } 202 | -------------------------------------------------------------------------------- /src/routes/user.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, Router } from 'express'; 2 | import { getUserModel, likeQuery, sequelize } from '@/db/model'; 3 | import { compact, set, toNumber } from 'lodash'; 4 | import { compareSync } from 'bcryptjs'; 5 | import { check } from '@/middlewares/auth-check'; 6 | 7 | const router = Router(); 8 | 9 | router.get('/get_user', async (req: Request, res: Response) => { 10 | const { identity, account } = req.user; 11 | const UserModel = getUserModel(identity); 12 | const user = await UserModel.findByPk(account, { 13 | attributes: { exclude: ['password', 'create_time', 'update_time'] }, 14 | }); 15 | res.json({ 16 | code: 200, 17 | msg: 'success', 18 | data: Object.assign({}, user?.toJSON(), req.user), 19 | }); 20 | }); 21 | 22 | router.post('/user/add', async (req: Request, res: Response) => { 23 | const { type, data } = req.body; 24 | if (!type || !data) { 25 | return res400(res); 26 | } 27 | const [exists] = await checkUser(type, [data]); 28 | if (exists.length !== 0) { 29 | return res.json({ 30 | code: 1, 31 | msg: '用户已存在', 32 | }); 33 | } 34 | const UserModel = getUserModel(type); 35 | await UserModel.create(data); 36 | res.json({ 37 | code: 200, 38 | msg: '添加成功', 39 | }); 40 | }); 41 | 42 | router.post('/user/import', async (req: Request, res: Response) => { 43 | const { type, data = [] } = req.body; 44 | if (!type || !data.length) { 45 | return res400(res); 46 | } 47 | const [exists, unexists] = await checkUser(type, data); 48 | console.log(exists, unexists); 49 | 50 | const UserModel = getUserModel(type); 51 | 52 | await sequelize.transaction(async transaction => { 53 | await UserModel.bulkCreate(unexists, { transaction, validate: true }); 54 | }); 55 | 56 | if (exists.length !== 0) { 57 | return res.json({ 58 | code: 1, 59 | msg: '用户已存在', 60 | data: exists, 61 | }); 62 | } 63 | 64 | res.json({ 65 | code: 200, 66 | msg: '添加成功', 67 | }); 68 | }); 69 | 70 | router.delete('/user/delete', async (req: Request, res: Response) => { 71 | const { type, data } = req.body; 72 | if (!Array.isArray(data.ids)) { 73 | return res400(res); 74 | } 75 | const { account, identity } = req.user; 76 | if (data.ids.includes(account) && type === identity) { 77 | return res.json({ 78 | code: 400, 79 | msg: '不能删除自己', 80 | }); 81 | } 82 | const UserModal = getUserModel(type); 83 | await UserModal.destroy({ 84 | where: { [UserModal.primaryKeyAttribute]: data.ids }, 85 | }); 86 | res.json({ 87 | code: 200, 88 | msg: '删除成功', 89 | }); 90 | }); 91 | 92 | router.get('/user/list', async (req: Request, res: Response) => { 93 | const { 94 | type, 95 | offset, 96 | limit, 97 | name, 98 | class: className, 99 | ...query 100 | } = req.query; 101 | 102 | Object.assign(query, likeQuery({ 103 | name, 104 | class: className, 105 | })); 106 | 107 | const Modal = getUserModel(type as string); 108 | const { rows, count } = await Modal.findAndCountAll({ 109 | attributes: { exclude: ['password'] }, 110 | where: query, 111 | order: [['create_time', 'DESC']], 112 | limit: toNumber(limit) || undefined, 113 | offset: toNumber(limit) * (toNumber(offset) - 1) || undefined, 114 | }); 115 | res.json({ 116 | code: 200, 117 | msg: '查询成功', 118 | count, 119 | data: rows.map(item => item.toJSON()), 120 | }); 121 | }); 122 | 123 | router.patch('/user/password', async (req: Request, res: Response) => { 124 | const { account, identity, oldVal, newVal } = req.body; 125 | const target: string[] = [account, identity, oldVal, newVal]; 126 | const { length } = compact(target); // 空值检测 127 | if (length !== target.length) { 128 | return res400(res); 129 | } 130 | const UserModal = getUserModel(identity); 131 | const user = await UserModal.findByPk(account); 132 | if (!user) { 133 | return res.json({ 134 | code: 2, 135 | msg: '用户不存在', 136 | }); 137 | } 138 | // 无匹配记录 139 | if (!compareSync(oldVal, user.getDataValue('password'))) { 140 | return res.json({ 141 | code: 1, 142 | msg: '原密码有误', 143 | }); 144 | } 145 | // 新密码加密后更新 146 | await UserModal.update({ password: newVal }, { 147 | where: { [UserModal.primaryKeyAttribute]: account }, 148 | }); 149 | res.json({ 150 | code: 200, 151 | msg: '修改成功', 152 | }); 153 | }); 154 | 155 | router.put('/user/reset', async (req: Request, res: Response) => { 156 | const { type, account } = req.body; 157 | const UserModel = getUserModel(type); 158 | // 重置密码 159 | await UserModel.update({ password: '123456' }, { 160 | where: { [UserModel.primaryKeyAttribute]: account }, 161 | }); 162 | res.json({ 163 | code: 200, 164 | msg: '重置成功', 165 | }); 166 | }); 167 | 168 | const checkUserUpdate = check('user:update'); 169 | router.put('/user/update', async (req: Request, res: Response) => { 170 | const { type, data } = req.body; 171 | if (!type || !data) { 172 | return res400(res); 173 | } 174 | 175 | const UserModel = getUserModel(type); 176 | const key = UserModel.primaryKeyAttribute; 177 | const { [key]: account, ...otherAttrs } = data; 178 | delete otherAttrs.password; // 密码修改使用单独的接口 179 | 180 | const isSelf = account === req.user.account && req.user.identity === type; 181 | const isPass = isSelf || checkUserUpdate(req); 182 | if (!isPass) { 183 | return res.json({ 184 | code: 401, 185 | msg: '暂无权限', 186 | }); 187 | } 188 | await UserModel.update(otherAttrs, { where: { [key]: account } }); 189 | res.json({ 190 | code: 200, 191 | msg: '修改成功', 192 | }); 193 | }); 194 | 195 | export default router; 196 | 197 | function res400(res: Response) { 198 | return res.json({ 199 | code: 400, 200 | msg: '参数有误', 201 | }); 202 | } 203 | 204 | /** 205 | * 判断用户是否已存在 206 | * @param type 用户类型 207 | * @param users 用户数据 208 | */ 209 | function checkUser(type: 'student' | 'teacher', users: any[]) { 210 | return new Promise<[Array, Array]>((resolve, reject) => { 211 | const model = getUserModel(type); 212 | const key = model.primaryKeyAttribute; 213 | model.findAll({ 214 | where: { [key]: users.map(item => item[key]) }, 215 | attributes: { exclude: ['password'] }, 216 | }).then((exist = []) => { 217 | const accounts = new Set(); 218 | resolve([ 219 | // 第一个参数是已存在的用户 220 | exist.map(item => { 221 | accounts.add(item.getDataValue(key)); 222 | return item.toJSON(); 223 | }), 224 | // 第二个参数是不存在的、待添加的用户 225 | users.filter(user => { 226 | if (accounts.has(user[key])) return false; 227 | // 设置默认身份 role_id为3是学生、4是教师 228 | set(user, 'role_id', type === 'student' ? 3 : 4); 229 | return true; 230 | }), 231 | ]); 232 | }).catch(reject); 233 | }); 234 | } 235 | -------------------------------------------------------------------------------- /src/config/init.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : 阿里云 5 | Source Server Type : MySQL 6 | 7 | Date: 30/05/2021 23:02:01 8 | */ 9 | 10 | SET NAMES utf8mb4; 11 | SET FOREIGN_KEY_CHECKS = 0; 12 | 13 | -- ---------------------------- 14 | -- Table structure for permissions 15 | -- ---------------------------- 16 | DROP TABLE IF EXISTS `permissions`; 17 | CREATE TABLE `permissions` ( 18 | `id` int(11) NOT NULL AUTO_INCREMENT, 19 | `label` varchar(255) NOT NULL, 20 | `action` enum('add','delete','update','query','import','export') NOT NULL, 21 | `type` enum('user','role','race','record','permission') NOT NULL, 22 | PRIMARY KEY (`id`), 23 | UNIQUE KEY `label` (`label`), 24 | UNIQUE KEY `label_2` (`label`), 25 | UNIQUE KEY `label_3` (`label`) 26 | ) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8; 27 | 28 | -- ---------------------------- 29 | -- Records of permissions 30 | -- ---------------------------- 31 | BEGIN; 32 | INSERT INTO `permissions` VALUES (1, '添加用户', 'add', 'user'); 33 | INSERT INTO `permissions` VALUES (2, '删除用户', 'delete', 'user'); 34 | INSERT INTO `permissions` VALUES (3, '修改用户', 'update', 'user'); 35 | INSERT INTO `permissions` VALUES (4, '查询用户', 'query', 'user'); 36 | INSERT INTO `permissions` VALUES (7, '添加比赛', 'add', 'race'); 37 | INSERT INTO `permissions` VALUES (8, '删除比赛', 'delete', 'race'); 38 | INSERT INTO `permissions` VALUES (9, '更新比赛', 'update', 'race'); 39 | INSERT INTO `permissions` VALUES (10, '导入用户', 'import', 'user'); 40 | INSERT INTO `permissions` VALUES (11, '查询比赛', 'query', 'race'); 41 | INSERT INTO `permissions` VALUES (12, '添加参赛记录', 'add', 'record'); 42 | INSERT INTO `permissions` VALUES (13, '更新参赛记录', 'update', 'record'); 43 | INSERT INTO `permissions` VALUES (14, '查询参赛记录', 'query', 'record'); 44 | INSERT INTO `permissions` VALUES (15, '删除参赛记录', 'delete', 'record'); 45 | INSERT INTO `permissions` VALUES (17, '添加角色', 'add', 'role'); 46 | INSERT INTO `permissions` VALUES (18, '删除角色', 'delete', 'role'); 47 | INSERT INTO `permissions` VALUES (19, '更新角色', 'update', 'role'); 48 | INSERT INTO `permissions` VALUES (20, '查询角色', 'query', 'role'); 49 | INSERT INTO `permissions` VALUES (21, '添加权限', 'add', 'permission'); 50 | INSERT INTO `permissions` VALUES (22, '删除权限', 'delete', 'permission'); 51 | INSERT INTO `permissions` VALUES (23, '查询权限', 'query', 'permission'); 52 | INSERT INTO `permissions` VALUES (24, '修改权限', 'update', 'permission'); 53 | INSERT INTO `permissions` VALUES (25, '导出用户', 'export', 'user'); 54 | INSERT INTO `permissions` VALUES (26, '导出参赛记录', 'export', 'record'); 55 | INSERT INTO `permissions` VALUES (28, '导出比赛', 'export', 'race'); 56 | COMMIT; 57 | 58 | -- ---------------------------- 59 | -- Table structure for races 60 | -- ---------------------------- 61 | DROP TABLE IF EXISTS `races`; 62 | CREATE TABLE `races` ( 63 | `race_id` int(11) NOT NULL AUTO_INCREMENT, 64 | `title` varchar(255) NOT NULL, 65 | `sponsor` varchar(255) NOT NULL, 66 | `type` varchar(255) NOT NULL, 67 | `level` int(11) NOT NULL, 68 | `location` varchar(255) NOT NULL, 69 | `date` datetime NOT NULL, 70 | `description` varchar(255) DEFAULT NULL, 71 | `create_time` datetime NOT NULL, 72 | `update_time` datetime NOT NULL, 73 | PRIMARY KEY (`race_id`) 74 | ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; 75 | 76 | -- ---------------------------- 77 | -- Records of races 78 | -- ---------------------------- 79 | BEGIN; 80 | INSERT INTO `races` VALUES (2, '互联网+创新创业大赛', '软件学院', 'A', 3, '软件楼', '2021-04-30 05:30:22', '', '2021-04-25 05:30:29', '2021-04-30 13:11:49'); 81 | INSERT INTO `races` VALUES (3, '校园歌手大赛', '南昌大学', 'A', 1, '前湖校区艺术楼', '2021-05-04 13:12:15', '', '2021-04-30 13:12:31', '2021-04-30 13:12:31'); 82 | INSERT INTO `races` VALUES (4, '计算机程序设计大赛', '江西省', 'B', 2, '软件楼', '2021-05-28 13:13:18', '', '2021-04-30 13:13:32', '2021-04-30 13:13:32'); 83 | INSERT INTO `races` VALUES (5, '奥林匹克数学竞赛', '南昌大学附属中学', 'A', 2, '校内', '2022-04-08 13:15:02', '', '2021-04-30 13:15:14', '2021-04-30 13:15:40'); 84 | INSERT INTO `races` VALUES (6, '致青春主题演讲比赛', '南昌大学附属中学', 'A', 1, '校内', '2021-06-01 13:17:50', '', '2021-04-30 13:17:56', '2021-04-30 13:17:56'); 85 | INSERT INTO `races` VALUES (7, '校园歌手大赛', '软件学院', 'A', 1, '软件楼', '2021-05-31 01:30:41', '哈哈哈', '2021-05-30 01:30:48', '2021-05-30 01:30:48'); 86 | COMMIT; 87 | 88 | -- ---------------------------- 89 | -- Table structure for records 90 | -- ---------------------------- 91 | DROP TABLE IF EXISTS `records`; 92 | CREATE TABLE `records` ( 93 | `record_id` int(11) NOT NULL AUTO_INCREMENT, 94 | `status` int(11) DEFAULT '0', 95 | `score` varchar(255) DEFAULT NULL, 96 | `description` varchar(255) DEFAULT '', 97 | `create_time` datetime NOT NULL, 98 | `update_time` datetime NOT NULL, 99 | `sid` varchar(255) DEFAULT NULL, 100 | `tid` varchar(255) DEFAULT NULL, 101 | `race_id` int(11) DEFAULT NULL, 102 | PRIMARY KEY (`record_id`), 103 | KEY `sid` (`sid`), 104 | KEY `tid` (`tid`), 105 | KEY `race_id` (`race_id`), 106 | CONSTRAINT `records_ibfk_7` FOREIGN KEY (`sid`) REFERENCES `students` (`sid`) ON DELETE SET NULL ON UPDATE CASCADE, 107 | CONSTRAINT `records_ibfk_8` FOREIGN KEY (`tid`) REFERENCES `teachers` (`tid`) ON DELETE SET NULL ON UPDATE CASCADE, 108 | CONSTRAINT `records_ibfk_9` FOREIGN KEY (`race_id`) REFERENCES `races` (`race_id`) ON DELETE SET NULL ON UPDATE CASCADE 109 | ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; 110 | 111 | -- ---------------------------- 112 | -- Records of records 113 | -- ---------------------------- 114 | BEGIN; 115 | INSERT INTO `records` VALUES (6, 0, '一等奖', '', '2021-05-30 15:01:40', '2021-05-30 15:01:40', 'admin', NULL, 2); 116 | COMMIT; 117 | 118 | -- ---------------------------- 119 | -- Table structure for role_permission 120 | -- ---------------------------- 121 | DROP TABLE IF EXISTS `role_permission`; 122 | CREATE TABLE `role_permission` ( 123 | `permission_id` int(11) NOT NULL, 124 | `role_id` int(11) NOT NULL, 125 | PRIMARY KEY (`permission_id`,`role_id`), 126 | KEY `role_id` (`role_id`), 127 | CONSTRAINT `role_permission_ibfk_1` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, 128 | CONSTRAINT `role_permission_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE 129 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 130 | 131 | -- ---------------------------- 132 | -- Records of role_permission 133 | -- ---------------------------- 134 | BEGIN; 135 | INSERT INTO `role_permission` VALUES (1, 1); 136 | INSERT INTO `role_permission` VALUES (2, 1); 137 | INSERT INTO `role_permission` VALUES (3, 1); 138 | INSERT INTO `role_permission` VALUES (4, 1); 139 | INSERT INTO `role_permission` VALUES (7, 1); 140 | INSERT INTO `role_permission` VALUES (8, 1); 141 | INSERT INTO `role_permission` VALUES (9, 1); 142 | INSERT INTO `role_permission` VALUES (10, 1); 143 | INSERT INTO `role_permission` VALUES (11, 1); 144 | INSERT INTO `role_permission` VALUES (12, 1); 145 | INSERT INTO `role_permission` VALUES (13, 1); 146 | INSERT INTO `role_permission` VALUES (14, 1); 147 | INSERT INTO `role_permission` VALUES (15, 1); 148 | INSERT INTO `role_permission` VALUES (17, 1); 149 | INSERT INTO `role_permission` VALUES (18, 1); 150 | INSERT INTO `role_permission` VALUES (19, 1); 151 | INSERT INTO `role_permission` VALUES (20, 1); 152 | INSERT INTO `role_permission` VALUES (21, 1); 153 | INSERT INTO `role_permission` VALUES (22, 1); 154 | INSERT INTO `role_permission` VALUES (23, 1); 155 | INSERT INTO `role_permission` VALUES (24, 1); 156 | INSERT INTO `role_permission` VALUES (25, 1); 157 | INSERT INTO `role_permission` VALUES (26, 1); 158 | INSERT INTO `role_permission` VALUES (28, 1); 159 | INSERT INTO `role_permission` VALUES (1, 2); 160 | INSERT INTO `role_permission` VALUES (3, 2); 161 | INSERT INTO `role_permission` VALUES (4, 2); 162 | INSERT INTO `role_permission` VALUES (7, 2); 163 | INSERT INTO `role_permission` VALUES (9, 2); 164 | INSERT INTO `role_permission` VALUES (10, 2); 165 | INSERT INTO `role_permission` VALUES (11, 2); 166 | INSERT INTO `role_permission` VALUES (12, 2); 167 | INSERT INTO `role_permission` VALUES (13, 2); 168 | INSERT INTO `role_permission` VALUES (14, 2); 169 | INSERT INTO `role_permission` VALUES (4, 3); 170 | INSERT INTO `role_permission` VALUES (11, 3); 171 | INSERT INTO `role_permission` VALUES (12, 3); 172 | INSERT INTO `role_permission` VALUES (14, 3); 173 | INSERT INTO `role_permission` VALUES (4, 4); 174 | INSERT INTO `role_permission` VALUES (11, 4); 175 | INSERT INTO `role_permission` VALUES (13, 4); 176 | INSERT INTO `role_permission` VALUES (14, 4); 177 | COMMIT; 178 | 179 | -- ---------------------------- 180 | -- Table structure for roles 181 | -- ---------------------------- 182 | DROP TABLE IF EXISTS `roles`; 183 | CREATE TABLE `roles` ( 184 | `id` int(11) NOT NULL AUTO_INCREMENT, 185 | `label` varchar(255) NOT NULL, 186 | `description` varchar(255) DEFAULT NULL, 187 | PRIMARY KEY (`id`), 188 | UNIQUE KEY `label` (`label`), 189 | UNIQUE KEY `label_2` (`label`), 190 | UNIQUE KEY `label_3` (`label`), 191 | UNIQUE KEY `label_4` (`label`), 192 | UNIQUE KEY `label_5` (`label`), 193 | UNIQUE KEY `label_6` (`label`), 194 | UNIQUE KEY `label_7` (`label`), 195 | UNIQUE KEY `label_8` (`label`), 196 | UNIQUE KEY `label_9` (`label`), 197 | UNIQUE KEY `label_10` (`label`), 198 | UNIQUE KEY `label_11` (`label`), 199 | UNIQUE KEY `label_12` (`label`), 200 | UNIQUE KEY `label_13` (`label`), 201 | UNIQUE KEY `label_14` (`label`), 202 | UNIQUE KEY `label_15` (`label`), 203 | UNIQUE KEY `label_16` (`label`), 204 | UNIQUE KEY `label_17` (`label`), 205 | UNIQUE KEY `label_18` (`label`), 206 | UNIQUE KEY `label_19` (`label`), 207 | UNIQUE KEY `label_20` (`label`), 208 | UNIQUE KEY `label_21` (`label`), 209 | UNIQUE KEY `label_22` (`label`), 210 | UNIQUE KEY `label_23` (`label`), 211 | UNIQUE KEY `label_24` (`label`), 212 | UNIQUE KEY `label_25` (`label`), 213 | UNIQUE KEY `label_26` (`label`), 214 | UNIQUE KEY `label_27` (`label`), 215 | UNIQUE KEY `label_28` (`label`), 216 | UNIQUE KEY `label_29` (`label`), 217 | UNIQUE KEY `label_30` (`label`), 218 | UNIQUE KEY `label_31` (`label`), 219 | UNIQUE KEY `label_32` (`label`), 220 | UNIQUE KEY `label_33` (`label`), 221 | UNIQUE KEY `label_34` (`label`), 222 | UNIQUE KEY `label_35` (`label`), 223 | UNIQUE KEY `label_36` (`label`), 224 | UNIQUE KEY `label_37` (`label`), 225 | UNIQUE KEY `label_38` (`label`), 226 | UNIQUE KEY `label_39` (`label`), 227 | UNIQUE KEY `label_40` (`label`), 228 | UNIQUE KEY `label_41` (`label`), 229 | UNIQUE KEY `label_42` (`label`), 230 | UNIQUE KEY `label_43` (`label`), 231 | UNIQUE KEY `label_44` (`label`), 232 | UNIQUE KEY `label_45` (`label`), 233 | UNIQUE KEY `label_46` (`label`), 234 | UNIQUE KEY `label_47` (`label`), 235 | UNIQUE KEY `label_48` (`label`), 236 | UNIQUE KEY `label_49` (`label`), 237 | UNIQUE KEY `label_50` (`label`), 238 | UNIQUE KEY `label_51` (`label`), 239 | UNIQUE KEY `label_52` (`label`), 240 | UNIQUE KEY `label_53` (`label`), 241 | UNIQUE KEY `label_54` (`label`), 242 | UNIQUE KEY `label_55` (`label`), 243 | UNIQUE KEY `label_56` (`label`), 244 | UNIQUE KEY `label_57` (`label`), 245 | UNIQUE KEY `label_58` (`label`), 246 | UNIQUE KEY `label_59` (`label`), 247 | UNIQUE KEY `label_60` (`label`), 248 | UNIQUE KEY `label_61` (`label`), 249 | UNIQUE KEY `label_62` (`label`), 250 | UNIQUE KEY `label_63` (`label`) 251 | ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; 252 | 253 | -- ---------------------------- 254 | -- Records of roles 255 | -- ---------------------------- 256 | BEGIN; 257 | INSERT INTO `roles` VALUES (1, '超级管理员', '系统管理员,可进行任何操作'); 258 | INSERT INTO `roles` VALUES (2, '普通管理员', '不能删除,不能操作角色和权限'); 259 | INSERT INTO `roles` VALUES (3, '学生', ''); 260 | INSERT INTO `roles` VALUES (4, '教师', ''); 261 | COMMIT; 262 | 263 | -- ---------------------------- 264 | -- Table structure for students 265 | -- ---------------------------- 266 | DROP TABLE IF EXISTS `students`; 267 | CREATE TABLE `students` ( 268 | `sid` varchar(255) NOT NULL, 269 | `name` varchar(255) NOT NULL, 270 | `password` varchar(255) NOT NULL DEFAULT '123456', 271 | `sex` int(11) NOT NULL, 272 | `grade` int(11) NOT NULL, 273 | `class` varchar(255) NOT NULL, 274 | `create_time` datetime NOT NULL, 275 | `update_time` datetime NOT NULL, 276 | `role_id` int(11) DEFAULT NULL, 277 | PRIMARY KEY (`sid`), 278 | KEY `role_id` (`role_id`), 279 | CONSTRAINT `students_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 280 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 281 | 282 | -- ---------------------------- 283 | -- Records of students 284 | -- ---------------------------- 285 | BEGIN; 286 | INSERT INTO `students` VALUES ('admin', '张三', '$2a$10$NrxfdEr1iiv47sazb2cRFOigpgOU6A5c2qOaaxYkvTOuWIhvROzJq', 1, 1, '1709', '2021-05-30 14:58:09', '2021-05-30 14:58:09', 1); 287 | COMMIT; 288 | 289 | -- ---------------------------- 290 | -- Table structure for teachers 291 | -- ---------------------------- 292 | DROP TABLE IF EXISTS `teachers`; 293 | CREATE TABLE `teachers` ( 294 | `tid` varchar(255) NOT NULL, 295 | `name` varchar(255) NOT NULL, 296 | `password` varchar(255) NOT NULL DEFAULT '123456', 297 | `rank` int(11) NOT NULL DEFAULT '0', 298 | `description` varchar(255) DEFAULT NULL, 299 | `create_time` datetime NOT NULL, 300 | `update_time` datetime NOT NULL, 301 | `role_id` int(11) DEFAULT NULL, 302 | PRIMARY KEY (`tid`), 303 | KEY `role_id` (`role_id`), 304 | CONSTRAINT `teachers_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE SET NULL ON UPDATE CASCADE 305 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 306 | 307 | -- ---------------------------- 308 | -- Records of teachers 309 | -- ---------------------------- 310 | BEGIN; 311 | COMMIT; 312 | 313 | SET FOREIGN_KEY_CHECKS = 1; 314 | --------------------------------------------------------------------------------