shaogen1995 14 timmar sedan
incheckning
2d257d31ed

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+/uploads
+
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

BIN
.nvmrc


+ 8 - 0
.prettierrc

@@ -0,0 +1,8 @@
+{
+  "semi": true,
+  "singleQuote": true,
+  "trailingComma": "es5",
+  "printWidth": 100,
+  "tabWidth": 2,
+  "useTabs": false
+}

+ 4 - 0
.vscode/settings.json

@@ -0,0 +1,4 @@
+{
+  "editor.defaultFormatter": "esbenp.prettier-vscode",
+  "editor.formatOnSave": true
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 3736 - 0
package-lock.json


+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "appdemo",
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "cross-env NODE_ENV=development tsx watch src/app.ts",
+    "dev2": "tsx watch src/app.ts",
+    "build": "tsc && shx cp package.json package-lock.json ./build/"
+  },
+  "devDependencies": {
+    "@types/cors": "^2.8.19",
+    "@types/express": "^5.0.6",
+    "@types/jsonwebtoken": "^9.0.10",
+    "@types/morgan": "^1.9.10",
+    "@types/multer": "^2.0.0",
+    "@types/node": "^25.0.3",
+    "shx": "^0.4.0",
+    "tsx": "^4.21.0",
+    "typescript": "^5.9.3"
+  },
+  "dependencies": {
+    "cors": "^2.8.5",
+    "cross-env": "^10.1.0",
+    "dayjs": "^1.11.19",
+    "express": "^5.2.1",
+    "express-session": "^1.18.2",
+    "express-validator": "^7.3.1",
+    "jsonwebtoken": "^9.0.3",
+    "mongodb": "^7.0.0",
+    "mongoose": "^9.0.2",
+    "morgan": "^1.10.1",
+    "multer": "^2.0.2",
+    "sharp": "^0.34.5",
+    "svg-captcha": "^1.4.0",
+    "util": "^0.12.5"
+  }
+}

+ 46 - 0
src/app.ts

@@ -0,0 +1,46 @@
+import express from 'express';
+import cors from 'cors';
+import morgan from 'morgan';
+import router from './router/index.js';
+import { isEnv } from './config/config.default.js';
+import fs from 'fs';
+import path from 'path';
+import { getLocalIP } from './util/index.js';
+import { errorHandler } from './middleware/index.js';
+
+const app = express();
+
+// 解析客户端请求
+app.use(express.json());
+app.use(express.urlencoded());
+
+// 跨域中间件
+app.use(cors());
+
+// 日志中间件
+if (isEnv) app.use(morgan('dev'));
+else {
+  // 创建一个写入流(追加模式)
+  const accessLogStream = fs.createWriteStream(
+    path.join(import.meta.dirname, 'config/access.log'),
+    {
+      flags: 'a',
+    }
+  );
+
+  app.use(morgan('combined', { stream: accessLogStream }));
+}
+// 静态文件访问
+const uploadsDir = path.join(process.cwd(), 'uploads');
+router.use('/uploads', express.static(uploadsDir));
+
+// 导入路由
+app.use('/api/v1', router);
+
+// 最后,注册错误处理中间件
+app.use(errorHandler);
+
+app.listen(6789, () => {
+  console.log('Run http://localhost:6789');
+  console.log(`Run http://${getLocalIP()}:6789`);
+});

+ 0 - 0
src/config/access.log


+ 25 - 0
src/config/config.default.ts

@@ -0,0 +1,25 @@
+// 400 - 参数错误
+// 401 - 没有接口权限/没有传token/token失效
+// 500 - 服务器代码错误
+// 404 - 没有找到数据
+// 0   - 接口正常
+
+// 生成token的秘钥
+export const tokenKey = 'f9cb1541-b570-495b-b40e-d80c11275277';
+
+// 本地环境和生产环境
+export const isEnv = process.env.NODE_ENV === 'development';
+
+// 本地连接
+// const envUrl = 'mongodb://127.0.0.1:27017/demo1';
+
+// 连接测试服务器
+const envUrl =
+  'mongodb://root:4Dkk2021testproject%25@111.230.233.212:27017/anHui_guoJiaTongBuFuShe?authSource=admin';
+
+// 生产环境地址
+const buildUrl =
+  'mongodb://root:4Dkk2021testproject%25@111.230.233.212:27017/anHui_guoJiaTongBuFuShe?authSource=admin';
+
+// 服务器链接地址
+export const mongodbUrl = isEnv ? envUrl : buildUrl;

+ 77 - 0
src/controller/fileController.ts

@@ -0,0 +1,77 @@
+import dayjs from 'dayjs';
+import path from 'path';
+import resSend from '../util/resSend.js';
+import { isImageFile } from '../middleware/fileUpload.js';
+import { compressImage } from '../util/imageCompressor.js';
+import { Files } from '../model/index.js';
+
+const file = {
+  upload: async (req: any, res: any) => {
+    if (!req.file) return resSend(res, 404, '请选择要上传的文件');
+
+    const { isDb } = req.body;
+
+    const filePath = req.body.filePath || 'default';
+
+    const file = req.file;
+    const isImage = isImageFile(file.mimetype);
+    const fileSize = file.size;
+
+    const baseUrl = `${req.protocol}://${req.get('host')}`;
+    let result: any = {
+      originalUrl: `${baseUrl}/uploads/${path.basename(file.path)}`,
+      compressedUrl: null,
+      message: '文件上传成功',
+    };
+
+    // 如果是图片且大于500KB,进行压缩
+    if (isImage && fileSize > 500 * 1024) {
+      const compressedFilename = 'compressed-' + path.basename(file.filename);
+      const compressedPath = path.join(path.dirname(file.path), compressedFilename);
+
+      const compressionResult = await compressImage(file.path, compressedPath);
+
+      if (compressionResult.success) {
+        result.compressedUrl = `${baseUrl}/uploads/${compressedFilename}`;
+        result.compressedSize = compressionResult.finalSize;
+        result.message = `文件上传成功,图片已从${(fileSize / 1024).toFixed(2)}KB压缩至${(
+          (compressionResult.finalSize || 0) / 1024
+        ).toFixed(2)}KB`;
+      } else {
+        result.message = '文件上传成功,但图片压缩失败,仅返回原图';
+      }
+    }
+
+    // 如果需要存储到数据库
+    if (isDb === 'true') {
+      try {
+        const fileRecord = new Files({
+          filename: path.basename(file.filename),
+          originalName: file.originalname,
+          mimetype: file.mimetype,
+          size: file.size,
+          compressedSize: result.compressedSize || null,
+          path: file.path,
+          compressedPath: result.compressedUrl
+            ? path.join(path.dirname(file.path), path.basename(result.compressedUrl))
+            : null,
+          isImage: isImage,
+          updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+        });
+
+        await fileRecord.save();
+        result.dbRecordId = fileRecord._id;
+      } catch (dbError) {
+        console.error('数据库存储失败:', dbError);
+        result.dbError = '数据库存储失败,但文件已上传';
+      }
+    }
+
+    // 记录API描述(根据您之前的代码风格)
+    req.apiDescription = `文件上传-${file.originalname}`;
+
+    return resSend(res, 0, result.message, result);
+  },
+};
+
+export default file;

+ 5 - 0
src/controller/inedx.ts

@@ -0,0 +1,5 @@
+import user from './userController.js';
+import issue from './issueController.js';
+import file from './fileController.js';
+
+export { user, issue, file };

+ 24 - 0
src/controller/issueController.ts

@@ -0,0 +1,24 @@
+import dayjs from 'dayjs';
+import { Dict } from '../model/index.js';
+import resSend from '../util/resSend.js';
+
+const issue = {
+  getIntro: async (req: any, res: any) => {
+    req.apiDescription = '内容发布-获取项目简介';
+    const introObj = await Dict.findById('694e4200f4ed1ea12901a424');
+
+    if (!introObj) return resSend(res, 404, '数据不存在');
+    return resSend(res, 0, '获取项目简介成功', introObj);
+  },
+  setIntro: async (req: any, res: any) => {
+    req.apiDescription = '内容发布-编辑项目简介';
+    const introObj = await Dict.findById('694e4200f4ed1ea12901a424');
+    if (!introObj) return resSend(res, 404, '数据不存在');
+    introObj.description = req.body.description || '';
+    introObj.updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
+    await introObj.save();
+    return resSend(res, 0, '编辑项目简介成功');
+  },
+};
+
+export default issue;

+ 343 - 0
src/controller/userController.ts

@@ -0,0 +1,343 @@
+import dayjs from 'dayjs';
+import { isEnv } from '../config/config.default.js';
+import { Log, User } from '../model/index.js';
+import { getTokenFu } from '../middleware/jwt.js';
+import { passWordJia, passWordJie } from '../util/pass.js';
+import resSend from '../util/resSend.js';
+import { generateCaptcha, ipLocResFu } from '../util/index.js';
+
+// 登录模块 需要做定时器处理,防止短时间多次发送
+let loginFlag: any = {};
+
+setTimeout(() => {
+  loginFlag = {};
+}, 60 * 1000 * 60 * 2);
+
+const loginTimeFu = (key: string) => {
+  if (loginFlag[key].loginFlagTime === 0) loginFlag[key].loginFlagTime = Date.now();
+  const nowTime = Date.now();
+  if (nowTime - loginFlag[key].loginFlagTime <= 60000) loginFlag[key].loginFlagNum++;
+};
+
+const user = {
+  getCode: async (req: any, res: any) => {
+    req.apiDescription = '用户模块-获取验证码';
+    const clientIp = ipLocResFu(req);
+
+    const captcha = generateCaptcha();
+    // 将验证码文本存入session(小写以便不区分大小写校验)
+    const captchaTxt = captcha.text.toLowerCase();
+    if (loginFlag[clientIp]) loginFlag[clientIp].loginFlagCode = captchaTxt;
+    else loginFlag[clientIp] = { loginFlagCode: captchaTxt };
+
+    // console.log('生成的验证码(开发调试用):', captchaTxt); // 调试时可查看
+
+    // 设置响应头,告诉浏览器这是SVG图片
+    res.type('svg');
+    res.send(captcha.data);
+  },
+  login: async (req: any, res: any) => {
+    req.apiDescription = '用户模块-用户登录';
+
+    const clientIp = ipLocResFu(req);
+    if (!loginFlag[clientIp]) {
+      loginFlag[clientIp] = {
+        loginFlagNum: 0,
+        loginFlagTime: 0,
+        loginFlagRef: null,
+        loginFlagCode: '',
+      };
+    }
+
+    if (loginFlag[clientIp].loginFlagNum >= 5) {
+      if (loginFlag[clientIp].loginFlagRef) clearTimeout(loginFlag[clientIp].loginFlagRef);
+
+      loginFlag[clientIp].loginFlagRef = setTimeout(() => {
+        loginFlag[clientIp].loginFlagNum = 0;
+        loginFlag[clientIp].loginFlagTime = 0;
+        loginFlag[clientIp].loginFlagRef = null;
+      }, 60 * 1 * 1000);
+      return resSend(res, 500, '请稍后重试');
+    } else {
+      const codeTxt = req.body.code.toLowerCase();
+
+      if (loginFlag[clientIp].loginFlagCode === codeTxt || (isEnv && codeTxt === '1111')) {
+        const dbUser = await User.findOne({
+          userName: req.body.userName,
+        }).select('+passWord');
+        if (dbUser && dbUser._id) {
+          const pass1 = passWordJie(dbUser.passWord);
+          const pass2 = passWordJie(req.body.passWord);
+
+          if (pass1 === pass2) {
+            loginFlag[clientIp].loginFlagCode = '';
+
+            loginFlag[clientIp].loginFlagNum = 0;
+            loginFlag[clientIp].loginFlagTime = 0;
+            const dbUserJson: any = dbUser.toObject();
+            delete dbUserJson.passWord;
+
+            // 获取token
+            const token = await getTokenFu(dbUserJson);
+
+            // 登录模块记录日志,由于没有token,需要特殊处理
+            req.userName = dbUserJson.userName;
+
+            return resSend(res, 0, '登录成功', { user: dbUserJson, token });
+          } else {
+            loginTimeFu(clientIp);
+            return resSend(res, 400, '密码错误');
+          }
+        } else {
+          loginTimeFu(clientIp);
+          return resSend(res, 400, '用户名错误');
+        }
+      } else {
+        loginTimeFu(clientIp);
+        return resSend(res, 400, '验证码错误');
+      }
+    }
+  },
+  loginText: async (req: any, res: any) => {
+    req.apiDescription = '用户模块-用户登录调试';
+
+    const dbUser = await User.findOne({
+      userName: req.body.userName,
+    });
+
+    if (dbUser && dbUser._id) {
+      const dbUserJson: any = dbUser.toJSON();
+
+      // 获取token
+      const token = await getTokenFu(dbUserJson);
+
+      return resSend(res, 0, '登录成功', { user: dbUserJson, token });
+    } else {
+      return resSend(res, 400, '用户名错误');
+    }
+  },
+
+  addOrEdit: async (req: any, res: any) => {
+    // console.log('xxxxxqqqxxx', JSON.stringify(req.body));
+    // 拿到参数模型
+
+    if (req.body._id) {
+      // 编辑用户
+      // 检查用户是否存在
+      const existingUser: any = await User.findById(req.body._id);
+      if (!existingUser) return resSend(res, 404, '用户不存在');
+      // 更新字段
+
+      // 过滤一些字段
+      const filetStr = ['userName', 'passWord'];
+
+      Object.keys(req.body).forEach((key) => {
+        if (key !== '_id' && req.body[key] !== undefined) {
+          if (!filetStr.includes(key)) {
+            existingUser[key] = req.body[key];
+          }
+        }
+      });
+
+      existingUser.updateTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
+      const updatedUser = await existingUser.save();
+
+      const userObj = updatedUser.toObject();
+      delete userObj.passWord;
+      req.apiDescription = `用户模块-编辑用户-${userObj.userName}`;
+      return resSend(res, 0, '编辑用户成功', userObj);
+    } else {
+      const userModel = new User({ ...req.body, passWord: passWordJia('Aa147852') });
+      // 新增用户
+      // 判断用户名是否已经存在
+      const dbUser = await User.findOne({ userName: req.body.userName });
+      if (dbUser && dbUser._id) {
+        return resSend(res, 400, '用户名已存在');
+      } else {
+        // 保存数据到数据库
+        const dbBack = await userModel.save();
+        // 将文档转换为普通对象并删除密码字段
+        const userObj = dbBack.toObject();
+        req.apiDescription = `用户模块-新增用户-${userObj.userName}`;
+        delete userObj.passWord;
+        return resSend(res, 0, '新增用户成功', userObj);
+      }
+    }
+  },
+  list: async (req: any, res: any) => {
+    req.apiDescription = '用户模块-获取用户列表';
+    // 拿到参数模型
+    // const userModel = new User(req.body);
+    const { pageNum = 1, pageSize = 10, searchKey = '' } = req.body;
+
+    // 构建查询条件
+    const query: any = {};
+    if (searchKey) {
+      // 使用正则表达式实现模糊查询,'i'表示不区分大小写
+      query.userName = { $regex: searchKey, $options: 'i' };
+    }
+
+    // 计算跳过的文档数量
+    const skip = (pageNum - 1) * pageSize;
+
+    // 并行执行:获取总条数和查询当前页数据
+    const [total, data] = await Promise.all([
+      // 获取满足条件的总记录数
+      User.countDocuments(query),
+      // 查询当前页数据
+      User.find(query).skip(skip).limit(parseInt(pageSize)).sort({ createdAt: -1 }), // 按创建时间倒序
+    ]);
+
+    // 计算总页数
+    const totalPages = Math.ceil(total / pageSize);
+
+    return resSend(res, 0, '获取用户列表成功', {
+      list: data,
+      pageNum: parseInt(pageNum),
+      pageSize: parseInt(pageSize),
+      total,
+      totalPages,
+    });
+  },
+  log: async (req: any, res: any) => {
+    req.apiDescription = '用户模块-获取操作日志';
+    // 拿到参数模型
+    // const userModel = new User(req.body);
+    const { pageNum = 1, pageSize = 10, searchKey = '', startTime = '', endTime = '' } = req.body;
+    // 构建查询条件
+    const query: any = {};
+
+    // 1. 处理模糊搜索条件
+    if (searchKey) {
+      query.userName = { $regex: searchKey, $options: 'i' }; // 'i'表示不区分大小写
+    }
+
+    // 2. 处理时间范围条件
+    if (startTime && endTime) {
+      query.createTime = {
+        $gte: startTime,
+        $lte: endTime,
+      };
+    }
+
+    // 计算跳过的文档数量
+    const skip = (pageNum - 1) * pageSize;
+
+    // 并行执行:获取总条数和查询当前页数据
+    const [total, data] = await Promise.all([
+      // 获取满足条件的总记录数
+      Log.countDocuments(query),
+      // 查询当前页数据,按时间倒序排列
+      Log.find(query).sort({ createTime: -1 }).skip(skip).limit(parseInt(pageSize)),
+    ]);
+
+    // 计算总页数
+    const totalPages = Math.ceil(total / pageSize);
+
+    return resSend(res, 0, '获取日志列表成功', {
+      list: data,
+      pageNum: parseInt(pageNum),
+      pageSize: parseInt(pageSize),
+      total,
+      totalPages,
+    });
+  },
+  resetPassWord: async (req: any, res: any) => {
+    const _id = req.params._id;
+    if (_id) {
+      const resPassWord = passWordJia('Aa147852');
+      const filter = { _id };
+      const update = {
+        $set: {
+          passWord: resPassWord,
+          updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+        },
+      };
+
+      const result = await User.findOneAndUpdate(filter, update);
+
+      if (result) {
+        req.apiDescription = `用户模块-重置密码-${result.userName}`;
+        return resSend(res, 0, '重置密码成功');
+      } else return resSend(res, 404, '未找到对应用户或密码未变更');
+    } else return resSend(res, 400, '用户_id不能为空');
+  },
+  editPassWord: async (req: any, res: any) => {
+    req.apiDescription = `用户模块-修改自己的密码`;
+    const dbUser = await User.findOne({
+      userName: req.user.userName,
+    }).select('+passWord');
+
+    const { oldPassword, newPassword } = req.body;
+    const oldPassRes = passWordJie(oldPassword);
+
+    const pass1 = passWordJie(dbUser!.passWord);
+
+    if (oldPassRes === pass1) {
+      const filter = { userName: req.user.userName };
+      const update = {
+        $set: {
+          passWord: newPassword,
+          updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+        },
+      };
+
+      const result = await User.updateOne(filter, update);
+
+      if (result.modifiedCount === 1) return resSend(res, 0, '修改密码成功');
+      else return resSend(res, 404, '未找到对应用户或密码未变更');
+    } else return resSend(res, 400, '旧密码错误');
+  },
+  del: async (req: any, res: any) => {
+    const { _id } = req.params; // 从URL参数中获取用户ID
+    // 1. 根据ID查找用户
+    const user = await User.findById(_id);
+    if (!user) return resSend(res, 404, '用户不存在');
+    if (user.isAdmin === 1) return resSend(res, 500, '管理员无法删除');
+
+    const deletedUser: any = await User.findByIdAndDelete(_id);
+    req.apiDescription = `用户模块-删除用户-${deletedUser.userName}`;
+    return resSend(res, 0, '删除用户成功');
+  },
+  setAuthority: async (req: any, res: any) => {
+    const { _id, authorityIds } = req.body;
+
+    if (!_id) return resSend(res, 400, '_id不能为空');
+
+    let authorityIdsRes: any[] = authorityIds || [];
+
+    authorityIdsRes = authorityIdsRes.map((v) => Number(v));
+
+    if (!authorityIdsRes.length) return resSend(res, 400, '权限数组不能为空');
+    const updatedUser = await User.findByIdAndUpdate(
+      _id, // 用户ID
+      {
+        authorityIds: authorityIdsRes,
+        updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      }, // 要更新的字段
+      {
+        new: true, // 返回更新后的文档
+        runValidators: true, // 确保更新操作也运行Schema级别的验证
+      }
+    );
+    if (!updatedUser) return resSend(res, 404, '未找到该用户');
+    req.apiDescription = `用户模块-修改用户页面权限-${updatedUser.userName}`;
+    return resSend(res, 0, '用户权限更新成功');
+  },
+  getInfo: async (req: any, res: any) => {
+    const { _id } = req.params;
+
+    if (!_id) return resSend(res, 400, '_id不能为空');
+
+    // 根据ID查询用户信息
+    const user = await User.findById(_id);
+
+    if (!user) {
+      return resSend(res, 404, '未找到该用户');
+    }
+    req.apiDescription = `用户模块-获取用户详情-${user.userName}`;
+    return resSend(res, 0, '获取用户详情成功', user);
+  },
+};
+
+export default user;

+ 39 - 0
src/middleware/fileUpload.ts

@@ -0,0 +1,39 @@
+import multer from 'multer';
+import path from 'path';
+import fs from 'fs';
+
+// 创建上传目录
+const uploadDir = path.join(process.cwd(), 'uploads');
+if (!fs.existsSync(uploadDir)) {
+  fs.mkdirSync(uploadDir, { recursive: true });
+}
+
+// 配置 Multer 存储策略
+const storage = multer.diskStorage({
+  destination: (req, file, cb) => {
+    cb(null, uploadDir);
+  },
+  filename: (req, file, cb) => {
+    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
+    const ext = path.extname(file.originalname);
+    cb(null, 'file-' + uniqueSuffix + ext);
+  },
+});
+
+// 文件过滤器
+const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
+  cb(null, true);
+};
+
+export const uploadZhong = multer({
+  storage: storage,
+  limits: {
+    fileSize: 500 * 1024 * 1024, // 500MB限制
+  },
+  fileFilter: fileFilter,
+});
+
+// 检查是否为图片文件
+export function isImageFile(mimetype: string): boolean {
+  return mimetype.startsWith('image/');
+}

+ 35 - 0
src/middleware/index.ts

@@ -0,0 +1,35 @@
+import { isEnv } from '../config/config.default.js';
+import { AddTxtFileFu } from '../util/index.js';
+import resSend from '../util/resSend.js';
+
+// 一些简单的中间件
+
+// 1:----------------有些接口只能管理员能操作
+export const isAdminZhong = (req: any, res: any, next: any) => {
+  if (req.user.isAdmin === 1) next();
+  else return resSend(res, 401, '没有接口权限');
+};
+
+// 2:----------------捕捉路由中的错误
+
+// 这个好像没啥用
+// export const asyncHandler = (fn: any) => (req: any, res: any, next: any) => {
+//   // 确保 fn 返回的 Promise 被捕获,任何异常都会通过 catch 传递给 next
+//   Promise.resolve(fn(req, res, next)).catch(next);
+// };
+
+export const errorHandler = (err: any, req: any, res: any, next: any) => {
+  // 记录错误日志,便于后端排查
+  // console.error('错误处理中间件捕获到异常:', err);
+  if (!isEnv) AddTxtFileFu(import.meta.dirname, '../config/access.log', err);
+
+  // 设置默认错误状态码和消息
+  const statusCode = err.statusCode || 500;
+  const msg = err.message || '服务器内部错误';
+
+  // 根据您的 resSend 格式返回错误
+  res.status(statusCode).json({
+    code: statusCode,
+    msg: msg,
+  });
+};

+ 41 - 0
src/middleware/jwt.ts

@@ -0,0 +1,41 @@
+// ------------token相关-----------------
+import jwt from 'jsonwebtoken';
+
+import { promisify } from 'util';
+import resSend from '../util/resSend.js';
+import { tokenKey } from '../config/config.default.js';
+import { User } from '../model/index.js';
+
+const toiwt: any = promisify(jwt.sign);
+
+const verify: any = promisify(jwt.verify);
+
+// 生成token
+export const getTokenFu = async (userInfo: any) => {
+  return await toiwt(userInfo, tokenKey, {
+    // token有效期10个小时
+    expiresIn: 60 * 60 * 10,
+  });
+};
+
+// 接口处理token中间件
+export const verifyToken = async (req: any, res: any, next: any) => {
+  // 获取请求头里面的token
+  let token = req.headers.token || '';
+
+  if (!token) return resSend(res, 401, '请传入用户凭证');
+
+  try {
+    const decoded = await verify(token, tokenKey);
+
+    const user = await User.findById(decoded._id);
+    if (!user) return resSend(res, 401, '用户不存在或已被删除');
+
+    // 用户信息存起来
+    req.user = user;
+
+    next();
+  } catch (error) {
+    return resSend(res, 401, '用户凭证无效');
+  }
+};

+ 38 - 0
src/middleware/requestLogger.ts

@@ -0,0 +1,38 @@
+import dayjs from 'dayjs';
+import { Log } from '../model/index.js';
+import { ipLocResFu } from '../util/index.js';
+
+const requestLogger = (req: any, res: any, next: any) => {
+  // 记录请求开始时间,用于计算响应时间
+  const startTime = Date.now();
+  // 重写 res.end 方法,以便在响应结束时获取状态码等信息
+  const originalEnd = res.end;
+  res.end = function (chunk: any, encoding: any) {
+    originalEnd.call(this, chunk, encoding); // 先执行原来的 res.end
+
+    // 响应结束后,计算耗时
+    const responseTime = Date.now() - startTime;
+
+    if (res.businessCode === 0) {
+      const obj: any = {
+        method: req.method,
+        url: req.originalUrl,
+        userName: req.userName || req.user.userName || '-',
+        apiDescription: req.apiDescription || '',
+        responseTime: responseTime,
+        ip: ipLocResFu(req), // 获取IP
+        userAgent: req.get('User-Agent'), // 获取User-Agent
+        createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      };
+      const newLog = new Log(obj);
+      newLog
+        .save()
+        .then(() => console.log(`请求日志已保存: ${req.method} ${req.originalUrl}`))
+        .catch((err) => console.error('保存请求日志失败:', err));
+    }
+  };
+
+  next(); // 继续处理请求
+};
+
+export default requestLogger;

+ 20 - 0
src/middleware/validator/errorBack.ts

@@ -0,0 +1,20 @@
+import { validationResult } from 'express-validator';
+// 所有接口参数 格式错误的处理函数
+const errorBack = (validator: any[]) => {
+  return async (req: any, res: any, next: any) => {
+    await Promise.all(validator.map((validata) => validata.run(req)));
+    const err = validationResult(req);
+    if (!err.isEmpty()) {
+      // console.log('qqqq', err.array());
+      const msg: string[] = [];
+      const key: string[] = [];
+      err.array().forEach((v: any) => {
+        msg.push(v.msg);
+        key.push(v.path);
+      });
+      return res.send({ code: 400, msg: msg.join(','), key: key.join(',') });
+    } else next();
+  };
+};
+
+export default errorBack;

+ 38 - 0
src/middleware/validator/userValidator.ts

@@ -0,0 +1,38 @@
+import { body } from 'express-validator';
+import errorBack from './errorBack.js';
+
+// 新增/编辑用户
+export const addUserVali = errorBack([
+  body('userName')
+    .notEmpty()
+    .withMessage('用户名不能为空')
+    .bail()
+    .isLength({ min: 5, max: 15 })
+    .withMessage('用户名长度为5~15个字符'),
+  // body('email')
+  //   .notEmpty()
+  //   .withMessage('邮箱不能为空')
+  //   .bail()
+  //   .isEmail()
+  //   .withMessage('邮箱格式不正确'),
+  // body('phone').notEmpty().withMessage('联系电话不能为空').bail(),
+]);
+
+// 登录
+export const loginVali = errorBack([
+  body('userName')
+    .notEmpty()
+    .withMessage('用户名不能为空')
+    .bail()
+    .isLength({ min: 5, max: 15 })
+    .withMessage('用户名长度为5~15个字符'),
+  body('passWord').notEmpty().withMessage('密码不能为空'),
+  body('code').isLength({ min: 4, max: 4 }).withMessage('验证码为4个字符'),
+  ,
+]);
+
+// 自改自己的密码
+export const editPassWord = errorBack([
+  body('oldPassword').notEmpty().withMessage('旧密码不能为空'),
+  body('newPassword').notEmpty().withMessage('新密码不能为空'),
+]);

+ 24 - 0
src/model/dictModel.ts

@@ -0,0 +1,24 @@
+import dayjs from 'dayjs';
+import mongoose from 'mongoose';
+
+const dictModel = new mongoose.Schema({
+  name: {
+    type: String,
+    require: true,
+  },
+  description: String,
+  sort: {
+    type: Number,
+    default: 999,
+  },
+  createTime: {
+    type: String,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+  updateTime: {
+    type: String,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+});
+
+export default dictModel;

+ 41 - 0
src/model/fileModel.ts

@@ -0,0 +1,41 @@
+import dayjs from 'dayjs';
+import mongoose from 'mongoose';
+
+const fileModel = new mongoose.Schema({
+  filename: {
+    type: String,
+    required: true,
+  },
+  originalName: {
+    type: String,
+    required: true,
+  },
+  mimetype: {
+    type: String,
+    required: true,
+  },
+  size: {
+    type: Number,
+    required: true,
+  },
+  compressedSize: {
+    type: Number,
+  },
+  path: {
+    type: String,
+    required: true,
+  },
+  compressedPath: {
+    type: String,
+  },
+  isImage: {
+    type: Boolean,
+    default: false,
+  },
+  updateTime: {
+    type: Date,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+});
+
+export default fileModel;

+ 23 - 0
src/model/index.ts

@@ -0,0 +1,23 @@
+import mongoose from 'mongoose';
+import userSchema from './userModel.js';
+import logSchema from './logModel.js';
+import { mongodbUrl } from '../config/config.default.js';
+import dictModel from './dictModel.js';
+import fileModel from './fileModel.js';
+
+const main = async () => {
+  await mongoose.connect(mongodbUrl);
+};
+
+main()
+  .then(() => {
+    console.log('链接成功');
+  })
+  .catch((err) => {
+    console.log('链接错误', err);
+  });
+
+export const User = mongoose.model('User', userSchema);
+export const Log = mongoose.model('Log', logSchema);
+export const Dict = mongoose.model('Dict', dictModel);
+export const Files = mongoose.model('File', fileModel);

+ 21 - 0
src/model/logModel.ts

@@ -0,0 +1,21 @@
+import dayjs from 'dayjs';
+import mongoose from 'mongoose';
+
+const logSchema = new mongoose.Schema({
+  method: String,
+  url: String,
+  // statusCode: Number,
+  userName: String, //操作用户
+  responseTime: Number, // 可选,响应时间
+  apiDescription: String, //操作事件中文
+  ip: String, // 可选,客户端IP
+  userAgent: String, // 可选,用户代理
+  createTime: {
+    // 可选,记录时间戳
+    type: String,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+  // 你可以根据需要添加更多字段,如用户ID、请求体/查询参数(注意脱敏)等
+});
+
+export default logSchema;

+ 49 - 0
src/model/userModel.ts

@@ -0,0 +1,49 @@
+import dayjs from 'dayjs';
+import mongoose from 'mongoose';
+
+const userSchema = new mongoose.Schema({
+  userName: {
+    type: String,
+    require: true,
+  },
+  passWord: {
+    type: String,
+    require: true,
+    select: false, //查询的时候不返回这个参数
+  },
+  isAdmin: {
+    type: Number,
+    default: 0,
+  },
+  realName: {
+    type: String,
+    default: '',
+  },
+  // 页面权限
+  authorityIds: {
+    type: [Number],
+    default: [100, 200, 300, 400, 500, 600, 700],
+  },
+  // email: {
+  //   type: String,
+  //   require: true,
+  // },
+  // phone: {
+  //   type: String,
+  //   require: true,
+  // },
+  // image: {
+  //   type: String,
+  //   default: '',
+  // },
+  createTime: {
+    type: String,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+  updateTime: {
+    type: String,
+    default: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  },
+});
+
+export default userSchema;

+ 13 - 0
src/router/file.ts

@@ -0,0 +1,13 @@
+import express from 'express';
+// 检测token的中间件
+import { verifyToken } from '../middleware/jwt.js';
+// 记录日志的中间件
+import requestLogger from '../middleware/requestLogger.js';
+import { file } from '../controller/inedx.js';
+import { uploadZhong } from '../middleware/fileUpload.js';
+
+const fileRouter = express.Router();
+
+fileRouter.post('/upload', verifyToken, uploadZhong.single('file'), file.upload);
+
+export default fileRouter;

+ 12 - 0
src/router/index.ts

@@ -0,0 +1,12 @@
+import express from 'express';
+import userRouter from './user.js';
+import issueRouter from './issue.js';
+import fileRouter from './file.js';
+
+const router = express.Router();
+
+router.use('/user', userRouter);
+router.use('/issue', issueRouter);
+router.use('/file', fileRouter);
+
+export default router;

+ 40 - 0
src/router/issue.ts

@@ -0,0 +1,40 @@
+import express from 'express';
+// 检测token的中间件
+import { verifyToken } from '../middleware/jwt.js';
+// 记录日志的中间件
+import requestLogger from '../middleware/requestLogger.js';
+import { issue } from '../controller/inedx.js';
+
+const issueRouter = express.Router();
+
+issueRouter
+
+  // 获取项目简介
+  .get('/getIntro', verifyToken, issue.getIntro)
+  // 编辑项目简介
+  .post('/setIntro', verifyToken, requestLogger, issue.setIntro);
+
+// //获取验证码
+// .get('/getCode', user.getCode)
+// //用户登录
+// .post('/login', loginVali, requestLogger, user.login)
+// //登录- 调试获取token专用
+// .post('/loginText', user.loginText)
+// // 新增/编辑用户
+// .post('/addOrEdit', addUserVali, verifyToken, isAdminZhong, requestLogger, user.addOrEdit)
+// // 获取用户列表
+// .post('/list', verifyToken, isAdminZhong, user.list)
+// // 获取日志列表
+// .post('/log', verifyToken, isAdminZhong, user.log)
+// // 通过id重置用户密码
+// .get('/resetPassWord/:_id', verifyToken, isAdminZhong, requestLogger, user.resetPassWord)
+// // 修改自己的密码
+// .post('/editPassWord', editPassWord, verifyToken, requestLogger, user.editPassWord)
+// // 删除除了isAdmin为1的用户
+// .get('/del/:_id', verifyToken, isAdminZhong, requestLogger, user.del)
+// // 设置用户页面权限
+// .post('/setAuthority', verifyToken, isAdminZhong, requestLogger, user.setAuthority)
+// // 获取用户详情
+// .get('/getInfo/:_id', verifyToken, user.getInfo);
+
+export default issueRouter;

+ 38 - 0
src/router/user.ts

@@ -0,0 +1,38 @@
+import express from 'express';
+import { user } from '../controller/inedx.js';
+// 校验字段的中间件
+import { loginVali, addUserVali, editPassWord } from '../middleware/validator/userValidator.js';
+// 检测token的中间件
+import { verifyToken } from '../middleware/jwt.js';
+// 记录日志的中间件
+import requestLogger from '../middleware/requestLogger.js';
+// 有些接口只能isAdmin为1的用户才能发起
+import { isAdminZhong } from '../middleware/index.js';
+
+const userRouter = express.Router();
+
+userRouter
+  //获取验证码
+  .get('/getCode', user.getCode)
+  //用户登录
+  .post('/login', loginVali, requestLogger, user.login)
+  //登录- 调试获取token专用
+  .post('/loginText', user.loginText)
+  // 新增/编辑用户
+  .post('/addOrEdit', addUserVali, verifyToken, isAdminZhong, requestLogger, user.addOrEdit)
+  // 获取用户列表
+  .post('/list', verifyToken, isAdminZhong, user.list)
+  // 获取日志列表
+  .post('/log', verifyToken, isAdminZhong, user.log)
+  // 通过id重置用户密码
+  .get('/resetPassWord/:_id', verifyToken, isAdminZhong, requestLogger, user.resetPassWord)
+  // 修改自己的密码
+  .post('/editPassWord', editPassWord, verifyToken, requestLogger, user.editPassWord)
+  // 删除除了isAdmin为1的用户
+  .get('/del/:_id', verifyToken, isAdminZhong, requestLogger, user.del)
+  // 设置用户页面权限
+  .post('/setAuthority', verifyToken, isAdminZhong, requestLogger, user.setAuthority)
+  // 获取用户详情
+  .get('/getInfo/:_id', verifyToken, user.getInfo);
+
+export default userRouter;

+ 60 - 0
src/util/imageCompressor.ts

@@ -0,0 +1,60 @@
+import sharp from 'sharp';
+import fs from 'fs';
+
+/**
+ * 压缩图片到目标大小左右
+ * @param inputPath 输入文件路径
+ * @param outputPath 输出文件路径
+ * @param targetSize 目标大小(字节),默认100KB
+ */
+export async function compressImage(
+  inputPath: string,
+  outputPath: string,
+  targetSize: number = 100 * 1024
+): Promise<{ success: boolean; finalSize?: number; error?: string }> {
+  try {
+    let quality = 80;
+    let compressedSize = targetSize + 1;
+
+    // 获取图片信息
+    const metadata = await sharp(inputPath).metadata();
+    let width = metadata.width;
+    let height = metadata.height;
+
+    // 如果图片尺寸较大,先调整尺寸
+    if (width && height && (width > 2000 || height > 2000)) {
+      width = Math.round(width * 0.5);
+      height = Math.round(height * 0.5);
+    }
+
+    while (compressedSize > targetSize && quality > 10) {
+      await sharp(inputPath)
+        .resize(width, height, {
+          fit: 'inside',
+          withoutEnlargement: true,
+        })
+        .jpeg({
+          quality: quality,
+          mozjpeg: true,
+        })
+        .toFile(outputPath);
+
+      // 获取压缩后文件大小
+      const stats = fs.statSync(outputPath);
+      compressedSize = stats.size;
+
+      // 如果仍然大于目标大小,降低质量继续压缩
+      if (compressedSize > targetSize) {
+        quality -= 15;
+        if (quality < 10) quality = 10;
+      }
+    }
+
+    return { success: true, finalSize: compressedSize };
+  } catch (error) {
+    return {
+      success: false,
+      error: error instanceof Error ? error.message : '压缩失败',
+    };
+  }
+}

+ 63 - 0
src/util/index.ts

@@ -0,0 +1,63 @@
+import os from 'os';
+import fs from 'fs';
+import path from 'path';
+
+// 一些常用的给工具函数
+
+// 1:------------ 获取本机器192xxxx地址
+export const getLocalIP = () => {
+  const interfaces = os.networkInterfaces();
+  for (let interfaceName in interfaces) {
+    const interfaceList = interfaces[interfaceName];
+
+    if (interfaceList) {
+      for (let i = 0; i < interfaceList.length; i++) {
+        const alias = interfaceList[i];
+        if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
+          return alias.address;
+        }
+      }
+    }
+  }
+  return '0.0.0.0';
+};
+
+import svgCaptcha from 'svg-captcha';
+
+// 2:------------ 生成验证码
+export const generateCaptcha = () => {
+  const res = svgCaptcha.create({
+    size: 4, // 验证码长度
+    noise: 2, // 干扰线条数
+    color: true, // 验证码字体颜色
+    background: '#f0f0f0', // 背景颜色
+    ignoreChars: '0oO1ilI', // 排除容易混淆的字符
+    width: 120,
+    height: 40,
+  });
+
+  return res;
+};
+
+//3:------------  获取请求者ip 用来确定验证码是否正确
+export const ipLocResFu = (req: any) => {
+  // 获取IP,并处理可能的IPv6格式
+  let rawIp = req.socket.remoteAddress;
+  // 简单的处理,如果包含`::ffff:`则去除,否则直接使用
+  const clientIp = rawIp && rawIp.replace(/^.*:/, '');
+  // console.log('客户端IP:', clientIp);
+  return clientIp;
+};
+
+//4:------------ 追加内容写入文件
+export const AddTxtFileFu = (nowPath: any, fileName: string, content: string) => {
+  const filePath = path.join(nowPath, fileName);
+
+  // 在要追加的内容末尾加上换行符 `\n`
+  const dataToAppend = content + '\n';
+  fs.appendFile(filePath, dataToAppend, 'utf8', (err) => {
+    if (err) {
+      return;
+    }
+  });
+};

+ 110 - 0
src/util/pass.ts

@@ -0,0 +1,110 @@
+function randomWord(randomFlag: boolean, min: number, max: number = 15) {
+  let str = '';
+  let range = min;
+  const arr = [
+    '0',
+    '1',
+    '2',
+    '3',
+    '4',
+    '5',
+    '6',
+    '7',
+    '8',
+    '9',
+    'a',
+    'b',
+    'c',
+    'd',
+    'e',
+    'f',
+    'g',
+    'h',
+    'i',
+    'j',
+    'k',
+    'l',
+    'm',
+    'n',
+    'o',
+    'p',
+    'q',
+    'r',
+    's',
+    't',
+    'u',
+    'v',
+    'w',
+    'x',
+    'y',
+    'z',
+    'A',
+    'B',
+    'C',
+    'D',
+    'E',
+    'F',
+    'G',
+    'H',
+    'I',
+    'J',
+    'K',
+    'L',
+    'M',
+    'N',
+    'O',
+    'P',
+    'Q',
+    'R',
+    'S',
+    'T',
+    'U',
+    'V',
+    'W',
+    'X',
+    'Y',
+    'Z',
+  ];
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min;
+  }
+  for (var i = 0; i < range; i++) {
+    const pos = Math.round(Math.random() * (arr.length - 1));
+    str += arr[pos];
+  }
+  return str;
+}
+
+// 密码加密
+export const passWordJia = (strTemp: string, strv = '') => {
+  const str = btoa(unescape(encodeURIComponent(strTemp)));
+
+  const NUM = 2;
+  const front = randomWord(false, 8);
+  const middle = randomWord(false, 8);
+  const end = randomWord(false, 8);
+
+  const str1 = str.substring(0, NUM);
+  const str2 = str.substring(NUM);
+
+  if (strv) {
+    const strv1 = strv.substring(0, NUM);
+    const strv2 = strv.substring(NUM);
+    return [front + str2 + middle + str1 + end, front + strv2 + middle + strv1 + end];
+  }
+
+  const txt = front + str2 + middle + str1 + end;
+
+  return txt;
+};
+// 密码解密
+export const passWordJie = (str: any) => {
+  const NUM = 2;
+  const str1 = str.substring(8);
+  const str2 = str1.substring(0, str1.length - 8);
+  const front = str2.slice(-NUM);
+  const end = str2.substring(0, str2.length - 8 - NUM);
+  const txt = front + end;
+  return decodeURIComponent(escape(atob(txt)));
+};

+ 19 - 0
src/util/resSend.ts

@@ -0,0 +1,19 @@
+import dayjs from 'dayjs';
+
+const resSend = (res: any, code: number, message: string, data: any = null) => {
+  // 将业务状态码挂载到res对象上,这样日志中间件就能直接拿到
+  res.businessCode = code;
+  // 调用Express原生的status和json方法发送响应
+
+  const obj: any = {
+    code: code,
+    msg: message,
+    timestamp: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+  };
+
+  if (data) obj.data = data;
+
+  res.status(200).json(obj);
+};
+
+export default resSend;

+ 22 - 0
tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext",
+    "outDir": "./build",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "allowImportingTsExtensions": false, // 允许导入.ts文件
+    "noEmit": false, // 不输出.js文件   
+    "forceConsistentCasingInFileNames": true,
+    "allowSyntheticDefaultImports": true,
+    "verbatimModuleSyntax": true
+  },
+  "include": [
+    "**/*.ts"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
+}