├── 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