diff --git a/_doc/framework_使用文档.md b/_doc/framework_使用文档.md index d10661f..6de638c 100644 --- a/_doc/framework_使用文档.md +++ b/_doc/framework_使用文档.md @@ -1,1415 +1,686 @@ -# Node Core Framework 使用说明 - -## 📋 概述 - -`node-core-framework.js` 是一个基于 Koa2 + Sequelize + MySQL 的企业级 Node.js 后端框架,提供了完整的 MVC 架构、数据库管理、API 文档生成、权限验证等功能。 - -## 🚀 快速开始 - -### 1. 环境要求 - -- **Node.js**: >= 16.0.0 -- **MySQL**: >= 5.7 -- **Redis**: >= 5.0 (可选,用于缓存) - -### 2. 安装依赖 - -```bash -# 安装核心依赖 -npm install koa koa-body koa-router koa-static koa2-cors koa2-swagger-ui -npm install sequelize mysql2 redis jsonwebtoken dayjs swagger-jsdoc -``` - -### 3. 基础使用 - -```javascript -// app.js -const Framework = require('./dist/node-core-framework.js'); - -// 配置文件 -const config = { - // 数据库配置 - db: { - username: "your_username", - password: "your_password", - database: "your_database", - host: "localhost", - port: 3306, - dialect: "mysql", - timezone: '+08:00' - }, - - // Redis配置(可选) - redis: { - host: 'localhost', - port: 6379, - password: '', - db: 0 - }, - - // 日志路径 - logPath: './logs', - - // 基础URL - baseUrl: 'http://localhost:3001', - - // API路径配置 - apiPaths: [ - { - path: './controllers', - prefix: '/api', - authType: 'applet' - } - ], - - // 模型路径 - modelPaths: './models', - - // 允许的URL(无需认证) - allowUrls: [ - '/api/users/login', - '/api/health', - '/api/docs' - ], - - // 授权文件路径 - license: { - licensePath: './license.lic' - } -}; - -// 启动应用 -async function startApp() { - try { - // 创建框架实例 - const framework = await Framework.init(config); - - // 启动服务器 - const server = await framework.start(3001); - - console.log('🎉 应用启动成功!'); - console.log(`📚 API 文档: http://localhost:3001/api/docs`); - - // 使用日志服务 - const logsService = Framework.getServices().logsService; - logsService.log('应用启动成功', { port: 3001, time: new Date() }); - - } catch (error) { - console.error('❌ 启动失败:', error); - - // 记录启动错误 - const logsService = Framework.getServices().logsService; - logsService.error('应用启动失败', error); - } -} - -startApp(); -``` - -## 🏗️ 框架架构 - -### 核心组件 - -1. **Framework**: 主框架类,负责整体协调 -2. **ModelManager**: 数据库模型管理器 -3. **ServiceManager**: 服务层管理器 -4. **RequestManager**: 请求处理器管理器 -5. **RegistrationService**: 授权验证服务 - -### 目录结构 - -``` -project/ -├── dist/ -│ └── node-core-framework.js # 打包后的框架文件 -├── controllers/ # 控制器目录 -│ ├── user_controller.js -│ ├── article_controller.js -│ └── admin/ -│ ├── sys_user.js # 系统用户管理 -│ └── sys_log.js # 日志管理接口 -├── models/ # 数据模型目录 -│ ├── user.js -│ └── article.js -├── config.js # 配置文件 -├── app.js # 应用入口 -└── logs/ # 日志目录 - ├── log_2024.01.15.log # 普通日志文件 - ├── logError_2024.01.15.log # 错误日志文件 - └── log_2024.01.15_1.log # 分割后的日志文件 -``` - -## 📝 详细配置说明 - -### 数据库配置 - -```javascript -const config = { - db: { - username: "root", // 数据库用户名 - password: "password", // 数据库密码 - database: "myapp", // 数据库名 - host: "localhost", // 数据库主机 - port: 3306, // 数据库端口 - dialect: "mysql", // 数据库类型 - timezone: '+08:00', // 时区 - pool: { // 连接池配置 - max: 10, // 最大连接数 - min: 0, // 最小连接数 - acquire: 30000, // 获取连接超时时间 - idle: 10000 // 连接空闲时间 - }, - logging: true // 是否打印SQL日志 - } -}; -``` - -### API路径配置 - -```javascript -const config = { - apiPaths: [ - { - path: './controllers', // 控制器目录路径 - prefix: '/api', // API前缀 - authType: 'applet' // 认证类型:applet | admin - }, - { - path: './controllers_admin', // 管理端控制器 - prefix: '/admin_api', // 管理端API前缀 - authType: 'admin' - } - ] -}; -``` - -### 权限配置 - -```javascript -const config = { - // 允许的URL(无需认证) - allowUrls: [ - '/api/users/login', // 登录接口 - '/api/users/register', // 注册接口 - '/api/health', // 健康检查 - '/api/docs', // API文档 - '/api/swagger.json' // Swagger配置 - ], - - // 授权文件路径 - license: { - licensePath: './license.lic' - } -}; -``` - -## 🎯 控制器开发 - -### 基础控制器结构 - -```javascript -// controllers/user_controller.js -const { models } = require('../database/db'); - -module.exports = { - /** - * @swagger - * /api/users: - * get: - * summary: 获取用户列表 - * description: 分页获取用户列表 - * tags: - * - 用户管理 - * parameters: - * - in: query - * name: page - * schema: - * type: integer - * default: 1 - * description: 页码 - * - in: query - * name: pageSize - * schema: - * type: integer - * default: 20 - * description: 每页数量 - * responses: - * 200: - * description: 获取成功 - */ - "GET /users": async (ctx) => { - const { user: UserModel } = models; - - // 获取分页参数 - const { limit, offset } = ctx.getPageSize(); - - // 查询用户列表 - const users = await UserModel.findAndCountAll({ - attributes: ['id', 'username', 'email', 'status', 'create_time'], - limit, - offset, - order: [['create_time', 'DESC']] - }); - - // 返回成功响应 - ctx.success(users, '获取用户列表成功'); - }, - - /** - * @swagger - * /api/users: - * post: - * summary: 创建用户 - * description: 创建新用户 - * tags: - * - 用户管理 - * requestBody: - * required: true - * content: - * application/json: - * schema: - * type: object - * required: - * - username - * - email - * - password - * properties: - * username: - * type: string - * description: 用户名 - * email: - * type: string - * format: email - * description: 邮箱 - * password: - * type: string - * description: 密码 - * responses: - * 200: - * description: 创建成功 - */ - "POST /users": async (ctx) => { - const { user: UserModel } = models; - const { username, email, password } = ctx.getBody(); - - // 参数验证 - if (!username || !email || !password) { - return ctx.fail('用户名、邮箱和密码不能为空'); - } - - // 检查邮箱是否已存在 - const existingUser = await UserModel.findOne({ where: { email } }); - if (existingUser) { - return ctx.fail('邮箱已存在'); - } - - // 创建用户 - const newUser = await UserModel.create({ - username, - email, - password, - status: 1 - }); - - ctx.success({ - id: newUser.id, - username: newUser.username, - email: newUser.email - }, '用户创建成功'); - } -}; -``` - -### 控制器方法说明 - -#### 请求方法格式 - -框架支持 **GET** 和 **POST** 两种请求方法,**推荐统一使用 POST 请求**: - -- `"POST /users"`: POST 请求(推荐) - - 参数通过请求体传递:`{ id: 123, username: "张三", pageOption: { page: 1, pageSize: 20 } }` - - 适用场景:所有接口(查询、创建、更新、删除等) - - 优点:参数传递统一、支持复杂数据结构、更安全 - -- `"GET /users"`: GET 请求(不推荐) - - 参数通过查询字符串传递:`?id=123&status=1` - - 仅在简单查询场景下使用 - -#### 上下文方法 - -```javascript -// 获取请求体数据 -const body = ctx.getBody(); - -// 获取查询参数 -const query = ctx.getQuery(); - -// 获取分页参数(从前端的 pageOption 中获取 page 和 pageSize) -// 前端发送格式:{ pageOption: { page: 1, pageSize: 20 } } -// 返回值:{ limit: 20, offset: 0 } -const { limit, offset } = ctx.getPageSize(); - -// 成功响应 -ctx.success(data, message); - -// 失败响应 -ctx.fail(message, code); -``` - -## 🗄️ 数据模型开发 - -### 模型定义 - -```javascript -// models/user.js -const Sequelize = require('sequelize'); - -module.exports = (db) => { - return db.define("user", { - id: { - type: Sequelize.INTEGER, - primaryKey: true, - autoIncrement: true, - comment: "用户ID" - }, - username: { - type: Sequelize.STRING(50), - allowNull: false, - comment: "用户名" - }, - email: { - type: Sequelize.STRING(100), - allowNull: false, - unique: true, - comment: "邮箱" - }, - password: { - type: Sequelize.STRING(255), - allowNull: false, - comment: "密码" - }, - avatar: { - type: Sequelize.STRING(255), - comment: "头像URL" - }, - status: { - type: Sequelize.INTEGER, - defaultValue: 1, - comment: "状态:0-禁用,1-启用" - } - }); -}; -``` - -### 模型关联 - -```javascript -// 在启动文件中定义关联关系 -const businessAssociations = (models) => { - // 用户和文章的一对多关系 - if (models.user && models.article) { - models.user.hasMany(models.article, { - foreignKey: 'author_id', - as: 'articles', - constraints: false // 不创建外键约束 - }); - - models.article.belongsTo(models.user, { - foreignKey: 'author_id', - as: 'author', - constraints: false - }); - } -}; - -// 在框架初始化时传入 -const framework = await Framework.init({ - // ... 其他配置 - businessAssociations: businessAssociations -}); -``` - -## 🔐 权限验证 - -### Token 验证 - -框架支持两种认证类型: -- `applet`: 小程序端认证 -- `admin`: 管理端认证 - -### 认证中间件 - -```javascript -// 在控制器中获取当前用户 -"POST /profile": async (ctx) => { - // 获取当前登录用户信息 - const currentUser = ctx.state.user; - - if (!currentUser) { - return ctx.fail('未登录', 401); - } - - ctx.success(currentUser, '获取用户信息成功'); -} -``` - -## 📊 API 文档 - -### Swagger 配置 - -框架自动生成 API 文档,访问地址:`http://localhost:3001/api/docs` - -### 自定义 Schema - -```javascript -const config = { - customSchemas: { - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "用户ID" - }, - "username": { - "type": "string", - "description": "用户名" - }, - "email": { - "type": "string", - "format": "email", - "description": "邮箱" - } - } - } - } -}; -``` - -## 🚀 部署配置 - -### 生产环境配置 - -```javascript -const config = { - env: 'production', - - db: { - // 生产数据库配置 - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_DATABASE, - host: process.env.DB_HOST, - port: process.env.DB_PORT, - dialect: "mysql", - logging: false // 生产环境关闭SQL日志 - }, - - redis: { - host: process.env.REDIS_HOST, - port: process.env.REDIS_PORT, - password: process.env.REDIS_PASSWORD - }, - - server: { - port: process.env.PORT || 3001, - host: '0.0.0.0' - } -}; -``` - -### PM2 部署 - -```javascript -// ecosystem.config.js -module.exports = { - apps: [{ - name: 'myapp', - script: 'app.js', - instances: 'max', - exec_mode: 'cluster', - env: { - NODE_ENV: 'production', - PORT: 3001 - } - }] -}; -``` - -```bash -# 启动应用 -pm2 start ecosystem.config.js - -# 查看状态 -pm2 status - -# 查看日志 -pm2 logs myapp -``` - -## 🔧 常用功能 - -### 日志服务 - -框架内置了完整的日志服务,支持普通日志、错误日志和请求日志记录。 - -#### 获取日志服务 - -```javascript -// 在控制器中获取日志服务 -const Framework = require('./dist/node-core-framework.js'); -const logsService = Framework.getServices().logsService; - - - -#### 基本日志记录 - -```javascript -// 普通日志 -logsService.log('用户登录成功', { userId: 123, username: 'admin' }); -logsService.log('数据更新完成', '用户信息已更新'); - -// 错误日志 -try { - // 业务逻辑 - throw new Error('数据库连接失败'); -} catch (error) { - logsService.error('数据库操作失败', error); -} - -// 上下文错误日志(包含请求信息) -logsService.ctxError(error, ctx); -``` - -#### 日志文件管理 - -```javascript -// 日志文件自动管理 -// - 按日期分割:log_2024.01.15.log -// - 错误日志:logError_2024.01.15.log -// - 文件大小超过5MB自动分割:log_2024.01.15_1.log -// - 自动创建日志目录 -``` - -#### 日志API接口 - -框架提供了日志管理的API接口: - -```javascript -// 获取所有日志文件列表 -GET /admin_api/sys_log/all - -// 查看日志文件内容 -GET /admin_api/sys_log/detail?title=log_2024.01.15.log - -// 删除指定日志文件 -GET /admin_api/sys_log/delete?title=log_2024.01.15.log - -// 删除所有日志文件 -GET /admin_api/sys_log/delete_all -``` - -#### 日志配置 - -```javascript -const config = { - // 日志目录配置 - logPath: './logs', // 相对路径,基于项目根目录 - - // 其他配置... -}; - -// 日志目录结构 -// project/ -// ├── logs/ -// │ ├── log_2024.01.15.log # 普通日志 -// │ ├── logError_2024.01.15.log # 错误日志 -// │ └── log_2024.01.15_1.log # 分割后的日志 -``` - -#### 实际使用示例 - -```javascript -// controller/user.js -const Framework = require('../framework'); -const logsService = Framework.getServices().logsService; - -module.exports = { - "POST /login": async (ctx) => { - try { - const { username, password } = ctx.getBody(); - - // 记录登录尝试 - logsService.log('用户登录尝试', { username, ip: ctx.ip }); - - // 验证用户 - const user = await UserModel.findOne({ where: { username } }); - - if (!user) { - logsService.log('登录失败:用户不存在', { username }); - return ctx.fail('用户名或密码错误'); - } - - // 验证密码 - const isValid = await bcrypt.compare(password, user.password); - - if (!isValid) { - logsService.log('登录失败:密码错误', { username }); - return ctx.fail('用户名或密码错误'); - } - - // 登录成功 - logsService.log('用户登录成功', { - userId: user.id, - username: user.username, - ip: ctx.ip - }); - - ctx.success({ token: generateToken(user) }, '登录成功'); - - } catch (error) { - // 记录系统错误 - logsService.ctxError(error, ctx); - ctx.fail('登录失败,请稍后重试'); - } - } -}; -``` - -### Token服务 - -框架内置了JWT Token服务,支持Token生成、验证、解析和微信数据解密。 - -#### 获取Token服务 - -```javascript -// 在控制器中获取Token服务 -const Framework = require('./dist/node-core-framework.js'); -const tokenService = Framework.getServices().tokenService; - -// 或者通过框架实例获取 -const framework = await Framework.init(config); -const tokenService = Framework.getServices().tokenService; -``` - -#### 基本Token操作 - -```javascript -// 创建Token -const userInfo = { id: 123, username: 'admin', role: 'admin' }; -const token = tokenService.create(userInfo); - -// 解析Token(获取用户信息) -const userData = tokenService.parse(token); -console.log(userData); // { id: 123, username: 'admin', role: 'admin' } - -// 验证Token(检查是否有效) -const isValid = tokenService.verify(token); -console.log(isValid); // true 或 false -``` - -#### 微信数据解密 - -```javascript -// 解密微信小程序数据 -const encryptedData = 'encrypted_data_from_wechat'; -const sessionKey = 'session_key_from_wechat'; -const iv = 'iv_from_wechat'; - -const decryptedData = tokenService.decryptWxData(encryptedData, sessionKey, iv); -console.log(decryptedData); // 解密后的用户信息 -``` - -#### MD5加密 - -```javascript -// MD5加密 -const password = '123456'; -const encryptedPassword = tokenService.getMd5(password); -console.log(encryptedPassword); // e10adc3949ba59abbe56e057f20f883e -``` - -#### 实际使用示例 - -```javascript -// controller/auth.js -const Framework = require('../framework'); -const tokenService = Framework.getServices().tokenService; - -module.exports = { - "POST /login": async (ctx) => { - try { - const { username, password } = ctx.getBody(); - - // 验证用户 - const user = await UserModel.findOne({ where: { username } }); - if (!user) { - return ctx.fail('用户不存在'); - } - - // 验证密码 - const isValidPassword = await bcrypt.compare(password, user.password); - if (!isValidPassword) { - return ctx.fail('密码错误'); - } - - // 生成Token - const userInfo = { - id: user.id, - username: user.username, - role: user.role - }; - const token = tokenService.create(userInfo); - - ctx.success({ token, user: userInfo }, '登录成功'); - - } catch (error) { - ctx.fail('登录失败'); - } - }, - - "POST /verify": async (ctx) => { - const { token } = ctx.getBody(); - - if (!token) { - return ctx.fail('缺少Token'); - } - - const userData = tokenService.parse(token); - if (!userData) { - return ctx.fail('Token无效'); - } - - ctx.success(userData, 'Token验证成功'); - } -}; -``` - -### Redis服务 - -框架内置了Redis缓存服务,支持数据缓存、分布式锁等功能。 - -#### 获取Redis服务 - -```javascript -// 在控制器中获取Redis服务 -const Framework = require('./dist/node-core-framework.js'); -const redisService = Framework.getServices().redisService; - -// 或者通过框架实例获取 -const framework = await Framework.init(config); -const redisService = Framework.getServices().redisService; -``` - -#### 基本缓存操作 - -```javascript -// 设置缓存(默认12小时过期) -await redisService.set('user:123', JSON.stringify({ name: '张三', age: 25 })); - -// 设置缓存(自定义过期时间,单位:秒) -await redisService.set('session:abc', 'session_data', 3600); // 1小时 - -// 获取缓存 -const userData = await redisService.get('user:123'); -console.log(JSON.parse(userData)); // { name: '张三', age: 25 } - -// 删除缓存 -await redisService.del('user:123'); -``` - -#### 原子操作 - -```javascript -// 原子设置(仅当key不存在时设置) -const success = await redisService.setIfAbsent('lock:order:123', 'locked', 300); -if (success) { - console.log('获取锁成功'); - // 执行业务逻辑 - await redisService.del('lock:order:123'); // 释放锁 -} else { - console.log('获取锁失败,资源被占用'); -} -``` - -#### 连接状态管理 - -```javascript -// 检查Redis连接状态 -const isConnected = redisService.isConnected(); -console.log('Redis连接状态:', isConnected); - -// 手动重连 -redisService.reconnect(); - -// 关闭连接 -redisService.close(); -``` - -#### 实际使用示例 - -```javascript -// controller/cache.js -const Framework = require('../framework'); -const redisService = Framework.getServices().redisService; - -module.exports = { - "POST /cache/user/detail": async (ctx) => { - const { id } = ctx.getBody(); // 从请求体获取 id - - if (!id) { - return ctx.fail('用户ID不能为空'); - } - - const cacheKey = `user:${id}`; - - try { - // 先尝试从缓存获取 - let userData = await redisService.get(cacheKey); - - if (userData) { - // 缓存命中 - console.log('从缓存获取用户数据'); - return ctx.success(JSON.parse(userData)); - } - - // 缓存未命中,从数据库获取 - const user = await UserModel.findByPk(id); - if (!user) { - return ctx.fail('用户不存在'); - } - - // 存入缓存(1小时过期) - await redisService.set(cacheKey, JSON.stringify(user.toJSON()), 3600); - - ctx.success(user); - - } catch (error) { - ctx.fail('获取用户信息失败'); - } - }, - - "POST /cache/clear": async (ctx) => { - const { pattern } = ctx.getBody(); - - // 清除指定模式的缓存 - // 注意:这里需要根据实际需求实现模式匹配删除 - await redisService.del(pattern); - - ctx.success(null, '缓存清除成功'); - } -}; -``` - -### Swagger服务 - -框架内置了Swagger API文档服务,支持自动生成API文档和Schema。 - -#### 获取Swagger服务 - -```javascript -// 在控制器中获取Swagger服务 -const Framework = require('./dist/node-core-framework.js'); -const swaggerService = Framework.getServices().createSwaggerService(); - -// 或者通过框架实例获取 -const framework = await Framework.init(config); -const swaggerService = Framework.getServices().createSwaggerService(); -``` - -#### 生成API文档 - -```javascript -// 生成Swagger规范 -const swaggerSpec = swaggerService.generateSpecs(req, res); - -// 获取Swagger配置 -const swaggerConfig = swaggerService.generateSwaggerConfig(req, res); - -// 获取Swagger选项 -const swaggerOptions = swaggerService.getSwaggerOptions(); -``` - -#### 安全配置管理 - -```javascript -// 获取当前安全配置 -const securityConfig = swaggerService.getCurrentSecurityConfig(req, res); - -// 更新安全配置 -const newConfig = [ - { 'admin-token': [] }, - { 'applet-token': [] } -]; -const result = swaggerService.updateSecurityConfig(req, res, newConfig); - -// 清除安全配置 -swaggerService.clearSecurityConfigCookie(res); -``` - -#### 实际使用示例 - -```javascript -// controller/docs.js -const Framework = require('../framework'); - -module.exports = { - "POST /api-docs": async (ctx) => { - const swaggerService = Framework.getServices().createSwaggerService(); - - // 生成Swagger规范 - const swaggerSpec = swaggerService.generateSpecs(ctx.request, ctx.response); - - ctx.response.type = 'application/json'; - ctx.response.body = swaggerSpec; - }, - - "POST /swagger-ui": async (ctx) => { - // 返回Swagger UI页面 - const swaggerUIManager = Framework.getSwaggerUIManager(); - const html = swaggerUIManager.getSwaggerUIHTML(); - - ctx.response.type = 'text/html'; - ctx.response.body = html; - } -}; -``` - -### 平台项目服务 - -框架内置了平台项目服务,支持与外部平台的交互。 - -#### 获取平台项目服务 - -```javascript -// 在控制器中获取平台项目服务 -const Framework = require('./dist/node-core-framework.js'); -const platformService = Framework.getServices().platformProjectService; - -// 或者通过框架实例获取 -const framework = await Framework.init(config); -const platformService = Framework.getServices().platformProjectService; -``` - -#### 基本平台操作 - -```javascript -// 获取所有模型 -const models = await platformService.modelAll(); - -// 生成表单 -const formData = await platformService.formGenerate({ id: 'form_id' }); - -// 创建表单 -const createResult = await platformService.createForm({ - name: '用户表单', - fields: ['name', 'email', 'phone'] -}); - -// 更新表单 -const updateResult = await platformService.updateForm({ - id: 'form_id', - name: '更新后的表单' -}); - -// 删除表单 -const deleteResult = await platformService.delteForm({ id: 'form_id' }); - -// 重新生成模型 -const modelResult = await platformService.modelGenerate({ id: 'model_id' }); -``` - -#### 文件下载 - -```javascript -// 下载平台文件 -const downloadResult = await platformService.downloadPlatformFile('/path/to/file'); -``` - -#### 实际使用示例 - -```javascript -// controller/platform.js -const Framework = require('../framework'); -const platformService = Framework.getServices().platformProjectService; - -module.exports = { - "POST /platform/models": async (ctx) => { - try { - const models = await platformService.modelAll(); - ctx.success(models); - } catch (error) { - ctx.fail('获取模型列表失败'); - } - }, - - "POST /platform/form": async (ctx) => { - try { - const formData = ctx.getBody(); - const result = await platformService.createForm(formData); - ctx.success(result, '表单创建成功'); - } catch (error) { - ctx.fail('表单创建失败'); - } - } -}; -``` - -### HTTP服务 - -框架内置了HTTP请求服务,支持各种HTTP请求方法。 - -#### 获取HTTP服务 - -```javascript -// 直接引入HTTP服务 -const { postPlatformUrl, downloadPlatformFile, post, get } = require('./services/http'); -``` - -#### 基本HTTP操作 - -```javascript -// GET请求 -const getData = await get('https://api.example.com/users', { page: 1, limit: 10 }); - -// POST请求 -const postData = await post('https://api.example.com/users', { - name: '张三', - email: 'zhangsan@example.com' -}); - -// POST表单数据 -const formData = await postFormData('https://api.example.com/upload', { - file: 'file_content', - name: 'filename.jpg' -}); -``` - -#### 平台API调用 - -```javascript -// 调用平台API -const platformData = await postPlatformUrl('/form/generate', { id: 'form_id' }); - -// 下载平台文件 -await downloadPlatformFile('/path/to/file', './local/path/file'); -``` - -#### 实际使用示例 - -```javascript -// controller/external.js -const { post, get } = require('../services/http'); - -module.exports = { - "POST /external/users": async (ctx) => { - try { - // 调用外部API - const users = await get('https://jsonplaceholder.typicode.com/users'); - ctx.success(users); - } catch (error) { - ctx.fail('获取外部数据失败'); - } - }, - - "POST /external/sync": async (ctx) => { - try { - const { data } = ctx.getBody(); - - // 同步数据到外部系统 - const result = await post('https://api.external.com/sync', data); - - ctx.success(result, '数据同步成功'); - } catch (error) { - ctx.fail('数据同步失败'); - } - } -}; -``` - -### 分页查询 - -框架提供了 `ctx.getPageSize()` 方法来获取分页参数。 - -**前端发送格式**: -```javascript -// 方式1: 通过 POST 请求体发送 -{ - pageOption: { - page: 1, // 页码,从1开始 - pageSize: 20 // 每页数量 - } -} - -// 方式2: 通过 GET 查询参数发送 -// GET /api/users?pageOption[page]=1&pageOption[pageSize]=20 -``` - -**后端使用示例**: -```javascript -"POST /users/list": async (ctx) => { - // ctx.getPageSize() 从前端的 pageOption 中获取 page 和 pageSize - // 返回值:{ limit: 20, offset: 0 } - // limit: 每页数量 (pageSize) - // offset: 跳过的记录数 ((page - 1) * pageSize) - const { limit, offset } = ctx.getPageSize(); - const { username, status } = ctx.getBody(); // 从请求体获取查询条件 - - // 构建查询条件 - const where = {}; - if (username) { - where.username = { [Op.like]: `%${username}%` }; - } - if (status !== undefined) { - where.status = status; - } - - const users = await UserModel.findAndCountAll({ - where, - limit, - offset, - order: [['create_time', 'DESC']] - }); - - ctx.success(users, '获取用户列表成功'); -} -``` - -### 文件上传 - -```javascript -"POST /upload": async (ctx) => { - const file = ctx.request.files.file; - - if (!file) { - return ctx.fail('请选择文件'); - } - - // 处理文件上传逻辑 - const fileName = `${Date.now()}_${file.name}`; - const filePath = `./upload/${fileName}`; - - // 保存文件 - const fs = require('fs'); - fs.writeFileSync(filePath, file.buffer); - - ctx.success({ - fileName, - filePath, - size: file.size - }, '文件上传成功'); -} -``` - -### 错误处理 - -```javascript -// 全局错误处理 -framework.app.on('error', (err, ctx) => { - console.error('服务器错误:', err); - - // 记录错误日志 - const logger = require('./services/logs'); - logger.error('服务器错误', { - error: err.message, - stack: err.stack, - url: ctx.url, - method: ctx.method - }); -}); -``` - -## 📝 最佳实践 - -### 1. 控制器设计 - -```javascript -// 好的实践:统一使用 POST 请求,清晰的路由命名 -module.exports = { - "POST /users/list": getUserList, // 获取用户列表 - "POST /users/detail": getUserDetail, // 获取用户详情 - "POST /users/create": createUser, // 创建用户 - "POST /users/update": updateUser, // 更新用户 - "POST /users/delete": deleteUser // 删除用户 -}; - -// 查询列表示例:支持分页和条件查询 -async function getUserList(ctx) { - const { limit, offset } = ctx.getPageSize(); // 从 pageOption 获取分页参数 - const { username, status } = ctx.getBody(); // 从请求体获取查询条件 - - // 构建查询条件 - const where = {}; - if (username) { - where.username = { [Op.like]: `%${username}%` }; - } - if (status !== undefined) { - where.status = status; - } - - const users = await UserModel.findAndCountAll({ - where, - limit, - offset, - order: [['create_time', 'DESC']] - }); - - ctx.success(users, '获取用户列表成功'); -} - -// 查询详情示例 -async function getUserDetail(ctx) { - const { id } = ctx.getBody(); // 从请求体获取 id - - if (!id) { - return ctx.fail('用户ID不能为空'); - } - - const user = await UserModel.findByPk(id); - if (!user) { - return ctx.fail('用户不存在'); - } - - ctx.success(user, '获取成功'); -} - -// 创建示例 -async function createUser(ctx) { - const { username, email, password } = ctx.getBody(); - - if (!username || !email || !password) { - return ctx.fail('用户名、邮箱和密码不能为空'); - } - - const newUser = await UserModel.create({ username, email, password }); - ctx.success(newUser, '创建成功'); -} - -// 更新示例 -async function updateUser(ctx) { - const { id, username, email } = ctx.getBody(); - - if (!id) { - return ctx.fail('用户ID不能为空'); - } - - await UserModel.update({ username, email }, { where: { id } }); - ctx.success(null, '更新成功'); -} - -// 删除示例 -async function deleteUser(ctx) { - const { id } = ctx.getBody(); - - if (!id) { - return ctx.fail('用户ID不能为空'); - } - - await UserModel.destroy({ where: { id } }); - ctx.success(null, '删除成功'); -} -``` - -### 2. 数据验证 - -```javascript -// 好的实践:参数验证 -"POST /users": async (ctx) => { - const { username, email, password } = ctx.getBody(); - - // 参数验证 - if (!username || !email || !password) { - return ctx.fail('用户名、邮箱和密码不能为空'); - } - - // 邮箱格式验证 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return ctx.fail('邮箱格式不正确'); - } - - // 业务逻辑... -} -``` - -### 3. 错误处理 - -```javascript -// 好的实践:统一的错误处理 -"POST /users": async (ctx) => { - try { - const { username, email, password } = ctx.getBody(); - - // 验证参数 - validateUserInput({ username, email, password }); - - // 检查用户是否存在 - await checkUserExists(email); - - // 创建用户 - const user = await createUser({ username, email, password }); - - ctx.success(user, '用户创建成功'); - - } catch (error) { - // 统一错误处理 - if (error.code === 'VALIDATION_ERROR') { - return ctx.fail(error.message, 400); - } - - if (error.code === 'USER_EXISTS') { - return ctx.fail(error.message, 409); - } - - // 未知错误 - console.error('创建用户失败:', error); - ctx.fail('服务器内部错误', 500); - } -} -``` - -## ❓ 常见问题 - -### Q1: 数据库连接失败 -**解决方案**: -1. 检查数据库服务是否启动 -2. 验证配置文件中的数据库连接信息 -3. 确认数据库用户权限 - -### Q2: 端口被占用 -**解决方案**: -```bash -# 查看端口占用 -netstat -ano | findstr :3001 - -# 终止进程 -taskkill /PID <进程ID> /F -``` - -### Q3: 模块找不到 -**解决方案**: -```bash -# 重新安装依赖 +徽标 +工单管理 +合并请求 +里程碑 +探索 +zc +/ +admin_core +代码 +工单 +合并请求 +Actions +软件包 +项目 +版本发布 +百科 +动态 +设置 +文件 +使用说明.md +完整使用文档.md +快速开始.md +.gitignore +README.md +babel.config.js +package-lock.json +package.json +postcss.config.js +webpack.config.js +admin_core +/ +_doc +/ +使用说明.md + +张成 +463d7921c1 +1 +1分钟前 +18 KiB +Admin Framework 使用说明 +一个基于 Vue2 的通用后台管理系统框架,包含完整的系统功能、登录、路由管理、布局等核心功能。 + +📦 框架特性 +✨ 核心功能 +✅ 简化的 API - 只需调用 createApp() 即可完成所有初始化 +✅ 模块化设计 - 组件、路由、状态管理等功能按模块组织 +✅ 完整的系统管理页面 - 用户、角色、菜单、日志等管理 +✅ 登录和权限管理 - 完整的登录流程和权限控制 +✅ 动态路由管理 - 基于权限菜单的动态路由生成 +✅ Vuex 状态管理 - 用户、应用状态管理 +✅ 全局组件库 - Tables、Editor、Upload、TreeGrid、FieldRenderer、FloatPanel 等 +✅ 工具库 - HTTP、日期、Token、Cookie 等工具 +✅ 内置样式 - base.less、animate.css、iconfont 等 +✅ 响应式布局 - 支持移动端适配 +🎯 内置页面组件 +主页组件 (HomePage) - 欢迎页面,显示系统标题 +系统管理页面 (SysUser, SysRole, SysLog, SysParamSetup) +高级管理页面 (SysMenu, SysControl, SysTitle) +登录页面 (LoginPage) +错误页面 (Page401, Page404, Page500) +🛠️ 内置工具 +HTTP 工具 (http) - 封装了 axios,支持拦截器、文件上传下载 +UI 工具 (uiTool) - 删除确认、树形转换、响应式设置、文件下载 +通用工具 (tools) - 日期格式化、UUID 生成、Cookie 操作、深拷贝等 +文件下载 - 支持 CSV 等格式的文件下载,自动处理换行符 +🚀 快速开始 +方式一:使用 Demo 项目(推荐) +我们提供了一个完整的 demo 项目,可以直接运行查看效果: + +# 1. 进入 demo 项目 +cd demo + +# 2. 安装依赖 npm install -# 清除缓存 -npm cache clean --force -``` +# 3. 启动开发服务器 +npm run dev +浏览器会自动打开 http://localhost:8080,查看: -### Q4: 权限验证失败 -**解决方案**: -1. 检查请求头中的 token -2. 确认 token 格式正确 -3. 验证 token 是否过期 +/login - 登录页面 +/home - 主页 +/system/user - 用户管理 +/ball/games - 业务示例页面 +方式二:构建框架 +# 1. 安装依赖 +npm install -## 📚 更多资源 +# 2. 构建框架 +npm run build -- [框架源码](https://github.com/your-repo/node-core-framework) -- [API 文档](http://localhost:3001/api/docs) -- [示例项目](./examples/) +# 3. 产物在 dist/admin-framework.js +🎯 极简使用方式 +只需 3 步即可完成集成! +1. 引入框架 +import AdminFramework from './admin-framework.js' +2. 创建应用 +const app = AdminFramework.createApp({ + title: '我的管理系统', + apiUrl: 'http://localhost:9098/admin_api/', + componentMap: { + 'business/product': ProductComponent, + 'business/order': OrderComponent + } +}) +3. 挂载应用 +app.$mount('#app') +就这么简单! 框架会自动完成所有初始化工作。 ---- +📖 完整使用指南 +1. 项目结构准备 +your-project/ +├── src/ +│ ├── config/ +│ │ └── index.js # 配置文件 +│ ├── libs/ +│ │ └── admin-framework.js # 框架文件 +│ ├── views/ +│ │ └── business/ # 业务页面 +│ ├── api/ +│ │ └── business/ # 业务 API +│ ├── App.vue +│ └── main.js +├── package.json +└── webpack.config.js +2. 安装依赖 +npm install vue vue-router vuex view-design axios dayjs js-cookie vuex-persistedstate +3. 创建配置文件 +在 src/config/index.js 中: -**🎉 恭喜!你已经掌握了 Node Core Framework 的基本使用方法!** +module.exports = { + title: '你的系统名称', + homeName: '首页', + apiUrl: 'http://localhost:9090/admin_api/', + uploadUrl: 'http://localhost:9090/admin_api/upload', + cookieExpires: 7, + uploadMaxLimitSize: 10, + oss: { + region: 'oss-cn-shanghai', + accessKeyId: 'your-key', + accessKeySecret: 'your-secret', + bucket: 'your-bucket', + url: 'http://your-bucket.oss-cn-shanghai.aliyuncs.com', + basePath: 'your-path/' + } +} +4. 创建 main.js(新版本 - 推荐) +import AdminFramework from './libs/admin-framework.js' -如有问题,请查看项目文档或提交 Issue。 +// 导入业务组件(根据权限菜单接口的 component 字段) +import GamesComponent from './views/ball/games.vue' +import PayOrdersComponent from './views/order/pay_orders.vue' + +// 🎉 只需一行代码!框架自动完成所有初始化 +const app = AdminFramework.createApp({ + title: '我的管理系统', + apiUrl: 'http://localhost:9098/admin_api/', + componentMap: { + 'ball/games': GamesComponent, + 'order/pay_orders': PayOrdersComponent + // 添加更多业务组件... + }, + onReady() { + console.log('应用已启动!') + // 应用启动完成后的回调 + } +}) + +// 挂载应用 +app.$mount('#app') +5. 创建 App.vue + + + + + + + +🔧 API 使用指南 +框架实例方法 +createApp(config) - 推荐使用 +创建应用实例(新版本 API) + +const app = AdminFramework.createApp({ + title: '我的管理系统', // 应用标题(必需) + apiUrl: 'http://localhost:9098/admin_api/', // API 基础地址(必需) + uploadUrl: 'http://localhost:9098/admin_api/upload', // 上传地址(可选,默认为 apiUrl + 'upload') + componentMap: { // 业务组件映射(可选) + 'business/product': ProductComponent, + 'business/order': OrderComponent + }, + onReady() { // 应用启动完成回调(可选) + console.log('应用已启动!') + } +}) +工具库使用 +HTTP 工具 +// 在组件中使用 +export default { + async mounted() { + // GET 请求 + const res = await this.$http.get('/api/users', { page: 1 }) + + // POST 请求 + const result = await this.$http.post('/api/users', { name: 'test' }) + + // 文件导出 + await this.$http.fileExport('/api/export', { type: 'excel' }) + } +} + +// 在非 Vue 组件中使用 +import AdminFramework from './libs/admin-framework.js' +const res = await AdminFramework.http.get('/api/users') +UI 工具 +// 在组件中使用 +export default { + methods: { + handleDelete() { + // 删除确认 + this.$uiTool.delConfirm(() => { + // 执行删除逻辑 + }) + + // 设置响应式字体 + this.$uiTool.setRem() + + // 树形转换 + const treeData = this.$uiTool.transformTree(flatData) + } + } +} +功能工具 +// 在组件中使用 +export default { + methods: { + downloadFile() { + // 文件下载 + this.$uiTool.downloadFile(response, 'filename.csv') + } + } +} +通用工具 +// 在组件中使用 +export default { + methods: { + formatDate() { + // 日期格式化 + return this.$tools.formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss') + }, + + generateId() { + // UUID 生成 + return this.$tools.generateUUID() + }, + + setCookie() { + // Cookie 操作 + this.$tools.setCookie('name', 'value') + const value = this.$tools.getCookie('name') + } + } +} +Store 模块使用 +user 模块 +// 登录 +await this.$store.dispatch('user/handleLogin', { + userFrom: { username, password }, + Main: AdminFramework.Main, + ParentView: AdminFramework.ParentView, + Page404: AdminFramework.Page404 +}) + +// 登出 +this.$store.dispatch('user/handleLogOut') + +// 设置权限菜单 +this.$store.dispatch('user/setAuthorityMenus', { + Main: AdminFramework.Main, + ParentView: AdminFramework.ParentView, + Page404: AdminFramework.Page404 +}) + +// 获取用户信息 +const userName = this.$store.getters['user/userName'] +const token = this.$store.state.user.token +app 模块 +// 设置面包屑 +this.$store.commit('app/setBreadCrumb', route) + +// 获取系统标题 +this.$store.dispatch('app/getSysTitle', { + defaultTitle: '系统名称', + defaultLogo: '/logo.png' +}) + +// 获取系统配置 +const sysFormModel = this.$store.getters['app/sysFormModel'] +🗂️ 组件映射配置 +业务组件映射 +当后端权限菜单接口返回组件路径时,需要配置映射表: + +// 1. 导入业务组件 +import GamesComponent from './views/ball/games.vue' +import PayOrdersComponent from './views/order/pay_orders.vue' + +// 2. 配置映射 +const componentMap = { + 'ball/games': GamesComponent, + 'ball/games.vue': GamesComponent, // 支持带 .vue 后缀 + 'order/pay_orders': PayOrdersComponent, + 'order/pay_orders.vue': PayOrdersComponent +} + +// 3. 在 Vue.use 时传入 +Vue.use(AdminFramework, { + config, + ViewUI, + VueRouter, + Vuex, + createPersistedState, + componentMap // 传入组件映射表 +}) +框架已自动映射的系统组件 +以下组件无需配置,框架已自动映射: + +✅ home/index - 主页 +✅ system/sys_user - 用户管理 +✅ system/sys_role - 角色管理 +✅ system/sys_log - 日志管理 +✅ system/sys_param_setup - 参数设置 +✅ system/sys_menu - 菜单管理 +✅ system/sys_control - 控制器管理 +✅ system/sys_title - 系统标题设置 +🌐 全局访问 +window.framework +框架实例会自动暴露到全局,可以在任何地方访问: + +// 在非 Vue 组件中使用 +const http = window.framework.http +const uiTool = window.framework.uiTool +const config = window.framework.config + +// HTTP 请求 +const res = await window.framework.http.get('/api/users') + +// UI 工具 +window.framework.uiTool.delConfirm(() => { + // 删除逻辑 +}) +Vue 原型方法 +在 Vue 组件中可以直接使用: + +export default { + methods: { + async loadData() { + // 直接使用 this.$xxx + const res = await this.$http.get('/api/users') + this.$uiTool.delConfirm(() => {}) + this.$tools.formatDate(new Date()) + this.$uiTool.downloadFile(response, 'file.csv') + } + } +} +📁 文件下载功能 +使用 downloadFile 方法 +框架提供了便捷的文件下载功能,支持 CSV 等格式: + +// 在 Vue 组件中使用 +export default { + methods: { + // 导出数据 + exportData() { + // 调用 API 获取数据 + this.$http.fileExport('/api/export', params).then(res => { + // 使用 downloadFile 下载 + this.$uiTool.downloadFile(res, '数据导出.csv') + this.$Message.success('导出成功!') + }).catch(error => { + this.$Message.error('导出失败:' + error.message) + }) + } + } +} +支持的数据格式 +CSV 格式:自动处理换行符,保持表格格式 +Blob 对象:支持二进制文件下载 +文本数据:支持纯文本文件下载 +自动处理特性 +✅ 换行符保持:CSV 文件的换行符会被正确保持 +✅ 文件名处理:自动清理文件名中的特殊字符 +✅ 浏览器兼容:支持所有现代浏览器 +✅ 内存管理:自动清理临时 URL 对象 +🎨 全局组件使用 +FloatPanel - 浮动面板组件 +FloatPanel 是一个浮动在父窗体上的面板组件,类似于抽屉效果,常用于详情展示、表单编辑等场景。 + +基本使用: + + + + 打开浮动面板 + + + 这里是面板内容 + + + + + +属性说明: + +属性 类型 默认值 说明 +title String '' 面板标题 +width String/Number '100%' 面板宽度(字符串或数字),默认占满父容器 +height String/Number '100%' 面板高度(字符串或数字),默认占满父容器 +position String 'right' 面板位置:left、right、top、bottom、center +showBack Boolean true 是否显示返回按钮 +showClose Boolean false 是否显示关闭按钮 +backText String '返回' 返回按钮文字 +closeOnClickBackdrop Boolean false 点击遮罩是否关闭 +mask Boolean false 是否显示遮罩(默认不显示) +zIndex Number 1000 层级 +方法: + +方法 说明 参数 +show(callback) 显示面板 callback: 可选的回调函数 +hide() 隐藏面板 - +事件: + +事件 说明 参数 +back 点击返回按钮时触发 - +插槽: + +插槽 说明 +default 面板主体内容 +header-right 头部右侧内容(可用于添加自定义按钮) +位置说明: + +left: 从左侧滑入 +right: 从右侧滑入(默认) +top: 从顶部滑入 +bottom: 从底部滑入 +center: 居中显示,带缩放动画 +完整示例: + + + + 查看详情 + + + + 保存 + + + + + + + + + + + + + + + + + +特性说明: + +✅ 基于父元素定位,不会遮挡菜单 +✅ 宽度和高度默认 100%,占满父容器 +✅ 无遮罩背景,完全浮在父页面上 +✅ 路由切换或组件销毁时自动关闭 +✅ 支持多种位置和动画效果 +✅ 支持自定义头部右侧内容 +📝 业务开发示例 +创建业务页面 + + + + 产品管理 + 加载数据 + + + + + +创建业务 API +// src/api/business/productServer.js +// 注意:不需要 import http,直接使用 http + +class ProductServer { + async getList(params) { + return await http.get('/product/list', params) + } + + async save(data) { + return await http.post('/product/save', data) + } + + async delete(id) { + return await http.post('/product/delete', { id }) + } + + async exportCsv(params) { + return await http.fileExport('/product/export', params) + } +} + +export default new ProductServer() +❓ 常见问题 +Q1: 打包后文件太大怎么办? +A: 框架已经将 Vue、VueRouter、Vuex、ViewUI、Axios 设置为外部依赖,不会打包进去。确保在项目中单独安装这些依赖。 + +Q2: 如何只使用部分功能? +A: 可以按需导入: + +import { http, uiTool, tools } from './libs/admin-framework.js' +Q3: 权限菜单中的业务页面显示 404 怎么办? +A: 需要配置组件映射表: + +Vue.use(AdminFramework, { + // ... 其他配置 + componentMap: { + 'ball/games': GamesComponent, + 'order/pay_orders': PayOrdersComponent + } +}) +Q4: 如何自定义配置? +A: 修改 config/index.js 文件: + +module.exports = { + title: '你的系统名称', + apiUrl: 'http://your-api-url/', + // ... 其他配置 +} +Q5: 如何使用登录功能? +A: 在组件中: + +export default { + methods: { + async login() { + await this.$store.dispatch('user/handleLogin', { + userFrom: { username: 'admin', password: '123456' }, + Main: AdminFramework.Main, + ParentView: AdminFramework.ParentView, + Page404: AdminFramework.Page404 + }) + this.$router.push({ name: 'home' }) + } + } +} +Q6: 需要单独引入样式文件吗? +A: 不需要! 框架已内置所有样式: + +✅ base.less - 基础样式 +✅ animate.css - 动画样式 +✅ ivewExpand.less - ViewUI 扩展样式 +✅ iconfont.css - 字体图标样式 +只需引入框架即可: + +import AdminFramework from './libs/admin-framework.js' +Vue.use(AdminFramework, { ... }) +📦 技术栈 +Vue 2.6+ +Vue Router 3.x +Vuex 3.x +View Design (iView) 4.x +Axios +Less +Webpack 5 +📄 许可证 +MIT License + +👨💻 作者 +light + +祝开发愉快! 🎉 + +如有问题,请查看 Demo 项目示例或联系开发团队。 + +Powered by Gitea +当前版本: +1.24.6 +页面: +273ms +模板: +13ms +许可证 +API \ No newline at end of file diff --git a/_sql/add_pla_account_json_config_fields.sql b/_sql/add_pla_account_json_config_fields.sql new file mode 100644 index 0000000..38935a7 --- /dev/null +++ b/_sql/add_pla_account_json_config_fields.sql @@ -0,0 +1,139 @@ +-- 为 pla_account 表添加 JSON 配置字段 +-- 执行时间:2025-01-XX +-- 说明:将自动投递、自动沟通、自动回复、自动活跃的配置项统一到 JSON 配置字段中 + +-- ============================================ +-- 添加自动投递配置字段(deliver_config) +-- ============================================ +-- 如果字段已存在则跳过,否则添加 +SET @exist_deliver_config = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pla_account' + AND COLUMN_NAME = 'deliver_config' +); + +SET @sql_deliver_config = IF(@exist_deliver_config = 0, + 'ALTER TABLE `pla_account` + ADD COLUMN `deliver_config` JSON COMMENT ''自动投递配置(JSON对象,包含:deliver_interval-投递间隔分钟数, min_salary-最低薪资, max_salary-最高薪资, page_count-滚动获取职位列表次数, max_deliver-每次最多投递数量, filter_keywords-过滤关键词, exclude_keywords-排除关键词)'' + AFTER `auto_deliver`', + 'SELECT ''字段 deliver_config 已存在,跳过添加'' AS message' +); + +PREPARE stmt FROM @sql_deliver_config; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================ +-- 添加自动沟通策略配置字段(chat_strategy) +-- ============================================ +SET @exist_chat_strategy = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pla_account' + AND COLUMN_NAME = 'chat_strategy' +); + +SET @sql_chat_strategy = IF(@exist_chat_strategy = 0, + 'ALTER TABLE `pla_account` + ADD COLUMN `chat_strategy` JSON COMMENT ''自动沟通策略配置(JSON对象,包含:chat_interval-沟通间隔分钟数, is_chat_outsourcing-是否沟通外包岗位, time_range-沟通时间段配置)'' + AFTER `auto_chat`', + 'SELECT ''字段 chat_strategy 已存在,跳过添加'' AS message' +); + +PREPARE stmt FROM @sql_chat_strategy; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================ +-- 添加自动回复配置字段(reply_config) +-- ============================================ +SET @exist_reply_config = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pla_account' + AND COLUMN_NAME = 'reply_config' +); + +SET @sql_reply_config = IF(@exist_reply_config = 0, + 'ALTER TABLE `pla_account` + ADD COLUMN `reply_config` JSON COMMENT ''自动回复配置(JSON对象)'' + AFTER `auto_reply`', + 'SELECT ''字段 reply_config 已存在,跳过添加'' AS message' +); + +PREPARE stmt FROM @sql_reply_config; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================ +-- 添加自动活跃动作配置字段(active_actions) +-- ============================================ +SET @exist_active_actions = ( + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'pla_account' + AND COLUMN_NAME = 'active_actions' +); + +SET @sql_active_actions = IF(@exist_active_actions = 0, + 'ALTER TABLE `pla_account` + ADD COLUMN `active_actions` TEXT COMMENT ''自动活跃动作配置(JSON对象,包含:active_interval-活跃间隔分钟数, actions-活跃动作列表)'' + AFTER `auto_active`', + 'SELECT ''字段 active_actions 已存在,跳过添加'' AS message' +); + +PREPARE stmt FROM @sql_active_actions; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- ============================================ +-- 数据迁移说明 +-- ============================================ +-- 如果需要将旧字段数据迁移到新 JSON 配置字段,可以执行以下 SQL: +-- +-- 1. 迁移自动投递配置(如果存在旧字段) +-- UPDATE pla_account +-- SET deliver_config = JSON_OBJECT( +-- 'deliver_interval', 30, +-- 'min_salary', COALESCE(min_salary, 0), +-- 'max_salary', COALESCE(max_salary, 0), +-- 'page_count', COALESCE(page_count, 3), +-- 'max_deliver', COALESCE(max_deliver, 10), +-- 'filter_keywords', COALESCE(filter_keywords, JSON_ARRAY()), +-- 'exclude_keywords', COALESCE(exclude_keywords, JSON_ARRAY()) +-- ) +-- WHERE deliver_config IS NULL AND (min_salary IS NOT NULL OR max_salary IS NOT NULL); +-- +-- 2. 迁移自动沟通配置(如果存在旧字段) +-- UPDATE pla_account +-- SET chat_strategy = JSON_OBJECT( +-- 'chat_interval', COALESCE(chat_interval, 30), +-- 'is_chat_outsourcing', COALESCE(is_chat_outsourcing, 0), +-- 'time_range', JSON_OBJECT( +-- 'start_time', '09:00', +-- 'end_time', '18:00', +-- 'workdays_only', 1 +-- ) +-- ) +-- WHERE chat_strategy IS NULL AND (chat_interval IS NOT NULL OR is_chat_outsourcing IS NOT NULL); +-- +-- 3. 迁移自动活跃配置(如果存在旧字段) +-- UPDATE pla_account +-- SET active_actions = CONCAT('{"active_interval":', COALESCE(active_interval, 60), ',"actions":[]}') +-- WHERE active_actions IS NULL OR active_actions = '' AND active_interval IS NOT NULL; + +-- ============================================ +-- 注意事项 +-- ============================================ +-- 1. deliver_config、chat_strategy、reply_config 使用 JSON 类型(MySQL 5.7+) +-- 2. active_actions 使用 TEXT 类型(兼容已有数据库结构) +-- 3. 如果 MySQL 版本低于 5.7,请将其他 JSON 类型改为 TEXT 类型 +-- 4. 执行前建议先备份数据库 +-- 5. 字段已存在时会自动跳过,不会报错 +-- 6. 如果需要迁移旧数据,请取消注释上面的数据迁移 SQL 并执行 + diff --git a/_sql/add_pla_account_json_config_fields_simple.sql b/_sql/add_pla_account_json_config_fields_simple.sql new file mode 100644 index 0000000..5fb3d73 --- /dev/null +++ b/_sql/add_pla_account_json_config_fields_simple.sql @@ -0,0 +1,79 @@ +-- 为 pla_account 表添加 JSON 配置字段(简化版) +-- 执行时间:2025-01-XX +-- 说明:将自动投递、自动沟通、自动回复、自动活跃的配置项统一到 JSON 配置字段中 +-- 注意:如果字段已存在,执行会报错,可以忽略或手动删除已存在的字段 + +-- ============================================ +-- 添加自动投递配置字段(deliver_config) +-- ============================================ +ALTER TABLE `pla_account` +ADD COLUMN `deliver_config` JSON COMMENT '自动投递配置(JSON对象,包含:deliver_interval-投递间隔分钟数, min_salary-最低薪资, max_salary-最高薪资, page_count-滚动获取职位列表次数, max_deliver-每次最多投递数量, filter_keywords-过滤关键词, exclude_keywords-排除关键词)' +AFTER `auto_deliver`; + +-- ============================================ +-- 添加自动沟通策略配置字段(chat_strategy) +-- ============================================ +ALTER TABLE `pla_account` +ADD COLUMN `chat_strategy` JSON COMMENT '自动沟通策略配置(JSON对象,包含:chat_interval-沟通间隔分钟数, is_chat_outsourcing-是否沟通外包岗位, time_range-沟通时间段配置)' +AFTER `auto_chat`; + +-- ============================================ +-- 添加自动回复配置字段(reply_config) +-- ============================================ +ALTER TABLE `pla_account` +ADD COLUMN `reply_config` JSON COMMENT '自动回复配置(JSON对象)' +AFTER `auto_reply`; + +-- ============================================ +-- 添加自动活跃动作配置字段(active_actions) +-- ============================================ +-- 如果字段已存在且为 TEXT 类型,则修改注释;如果不存在,则添加 +ALTER TABLE `pla_account` +ADD COLUMN `active_actions` TEXT COMMENT '自动活跃动作配置(JSON对象,包含:active_interval-活跃间隔分钟数, actions-活跃动作列表)' +AFTER `auto_active`; + +-- ============================================ +-- 初始化默认配置值(可选) +-- ============================================ +-- 为已有数据初始化默认配置(如果字段为空) +UPDATE `pla_account` +SET `deliver_config` = JSON_OBJECT( + 'deliver_interval', 30, + 'min_salary', 0, + 'max_salary', 0, + 'page_count', 3, + 'max_deliver', 10, + 'filter_keywords', JSON_ARRAY(), + 'exclude_keywords', JSON_ARRAY() +) +WHERE `deliver_config` IS NULL; + +UPDATE `pla_account` +SET `chat_strategy` = JSON_OBJECT( + 'chat_interval', 30, + 'is_chat_outsourcing', 0, + 'time_range', JSON_OBJECT( + 'start_time', '09:00', + 'end_time', '18:00', + 'workdays_only', 1 + ) +) +WHERE `chat_strategy` IS NULL; + +UPDATE `pla_account` +SET `reply_config` = JSON_OBJECT() +WHERE `reply_config` IS NULL; + +UPDATE `pla_account` +SET `active_actions` = '{"active_interval": 60, "actions": []}' +WHERE `active_actions` IS NULL OR `active_actions` = ''; + +-- ============================================ +-- 注意事项 +-- ============================================ +-- 1. deliver_config、chat_strategy、reply_config 使用 JSON 类型(MySQL 5.7+) +-- 2. active_actions 使用 TEXT 类型(兼容已有数据库结构) +-- 3. 如果 MySQL 版本低于 5.7,请将其他 JSON 类型改为 TEXT 类型 +-- 4. 执行前建议先备份数据库 +-- 5. 如果字段已存在,ALTER TABLE 会报错,可以忽略或先删除已存在的字段 + diff --git a/admin/config/index.js b/admin/config/index.js index ff14a7b..85b8313 100644 --- a/admin/config/index.js +++ b/admin/config/index.js @@ -26,11 +26,11 @@ const baseConfig = { // 开发环境配置 const developmentConfig = { ...baseConfig, - // apiUrl: 'http://localhost:9097/admin_api/', - // uploadUrl: 'http://localhost:9097/admin_api/upload', + apiUrl: 'http://localhost:9097/admin_api/', + uploadUrl: 'http://localhost:9097/admin_api/upload', - apiUrl: 'http://work.light120.com/admin_api/', - uploadUrl: 'http://work.light120.com/admin_api/upload', + // apiUrl: 'http://work.light120.com/admin_api/', + // uploadUrl: 'http://work.light120.com/admin_api/upload', // 开发环境显示更多调试信息 debug: true } diff --git a/admin/src/api/profile/pla_account_server.js b/admin/src/api/profile/pla_account_server.js index 666b35c..560b4da 100644 --- a/admin/src/api/profile/pla_account_server.js +++ b/admin/src/api/profile/pla_account_server.js @@ -31,7 +31,7 @@ class PlaAccountServer { * @returns {Promise} */ add(row) { - return window.framework.http.post('/account/create', row) + return window.framework.http.post('/account/add', row) } /** diff --git a/admin/src/views/account/pla_account.vue b/admin/src/views/account/pla_account.vue index 959d84e..eb5e8e3 100644 --- a/admin/src/views/account/pla_account.vue +++ b/admin/src/views/account/pla_account.vue @@ -1,8 +1,12 @@ - 新增账号 - 批量解析位置 + + 新增账号 + 批量解析位置 + + - + + + + + + @@ -216,8 +225,12 @@ + + + diff --git a/admin/src/views/home/index.vue b/admin/src/views/home/index.vue index 06174a0..cb662dd 100644 --- a/admin/src/views/home/index.vue +++ b/admin/src/views/home/index.vue @@ -19,6 +19,121 @@ + + + + 账户信息 + 查看详情 + + + + + 账户名 + {{ accountInfo.name || '-' }} + + + + + 平台 + + + {{ getPlatformText(accountInfo.platform_type) }} + + + + + + + 登录名 + {{ accountInfo.login_name || '-' }} + + + + + 设备SN码 + {{ accountInfo.sn_code || '-' }} + + + + + + + + 是否启用 + + + + + + 是否在线 + + + + + + 是否登录 + + + + + + 自动投递 + + + + + + 自动沟通 + + + + + + 自动活跃 + + + + + + @@ -56,9 +171,22 @@ - + 当前执行中的任务 - + + + + 暂无执行中的任务 + + + + 加载中... + @@ -73,6 +201,7 @@ import * as echarts from 'echarts' import DeviceStatusServer from '../../api/device/device_status_server.js' import StatisticsServer from '../../api/statistics/statistics_server.js' +import PlaAccountServer from '../../api/profile/pla_account_server.js' export default { name: 'HomePage', @@ -98,7 +227,16 @@ export default { taskLoading: false, chartInstance: null, // ECharts实例 taskColumns: [], - refreshTimer: null + refreshTimer: null, + accountInfo: {}, // 账户信息 + switchLoading: { + is_enabled: false, + is_online: false, + is_logged_in: false, + auto_deliver: false, + auto_chat: false, + auto_active: false + } } }, @@ -220,7 +358,8 @@ export default { await Promise.all([ this.loadTodayStats(), this.loadChartData(), - this.loadRunningTasks() + this.loadRunningTasks(), + this.loadAccountInfo() ]) }, @@ -567,6 +706,155 @@ export default { // 计算进度百分比 const progress = Math.round((completedCount / commands.length) * 100) return progress + }, + + // 加载账户信息 + async loadAccountInfo() { + if (!this.selectedDeviceSn) { + this.accountInfo = {} + return + } + + try { + // 根据 sn_code 查询账户信息 + const res = await PlaAccountServer.page({ + seachOption: { + key: 'sn_code', + value: this.selectedDeviceSn + }, + pageOption: { + page: 1, + pageSize: 1 + } + }) + + if (res.code === 0 && res.data && res.data.rows && res.data.rows.length > 0) { + const accountData = res.data.rows[0] + + // 如果接口没有返回设备状态,则单独查询 + if (accountData.is_online === undefined || accountData.is_logged_in === undefined) { + try { + const deviceRes = await DeviceStatusServer.getById(this.selectedDeviceSn) + if (deviceRes.code === 0 && deviceRes.data) { + accountData.is_online = deviceRes.data.isOnline || false + accountData.is_logged_in = deviceRes.data.isLoggedIn || false + } else { + accountData.is_online = false + accountData.is_logged_in = false + } + } catch (deviceError) { + console.error('获取设备状态失败:', deviceError) + accountData.is_online = false + accountData.is_logged_in = false + } + } + + // 确保布尔值字段正确初始化 + accountData.is_enabled = accountData.is_enabled !== undefined ? accountData.is_enabled : 1 + accountData.auto_deliver = accountData.auto_deliver !== undefined ? accountData.auto_deliver : 0 + accountData.auto_chat = accountData.auto_chat !== undefined ? accountData.auto_chat : 0 + accountData.auto_active = accountData.auto_active !== undefined ? accountData.auto_active : 0 + + this.accountInfo = accountData + } else { + this.accountInfo = {} + } + } catch (error) { + console.error('加载账户信息失败:', error) + this.accountInfo = {} + } + }, + + // 处理开关变化 + async handleSwitchChange(field, value) { + if (!this.accountInfo.id) { + this.$Message.warning('账户信息不存在') + return + } + + // is_online 和 is_logged_in 是只读的,不应该被修改 + if (field === 'is_online' || field === 'is_logged_in') { + this.$Message.warning('该状态由设备状态决定,无法手动修改') + // 恢复原值 + this.$nextTick(() => { + if (field === 'is_online') { + this.accountInfo.is_online = !value + } else { + this.accountInfo.is_logged_in = !value + } + }) + return + } + + // 设置加载状态 + this.switchLoading[field] = true + + try { + // 确保 value 是数字类型(0 或 1) + const numericValue = value ? 1 : 0 + + // 构建更新数据,确保是纯对象,只包含基本类型 + const updateData = { + id: Number(this.accountInfo.id), + [field]: numericValue + } + + // 调用更新接口 + await PlaAccountServer.update(updateData) + + // 更新本地数据 + this.accountInfo[field] = numericValue + + this.$Message.success('更新成功') + } catch (error) { + console.error(`更新${field}失败:`, error) + const errorMsg = error.response?.data?.message || error.message || '请稍后重试' + this.$Message.error('更新失败:' + errorMsg) + + // 恢复原值 + this.$nextTick(() => { + const oldValue = this.accountInfo[field] + if (field === 'is_enabled') { + this.accountInfo.is_enabled = oldValue === 1 ? 0 : 1 + } else if (field === 'auto_deliver') { + this.accountInfo.auto_deliver = oldValue === 1 ? 0 : 1 + } else if (field === 'auto_chat') { + this.accountInfo.auto_chat = oldValue === 1 ? 0 : 1 + } else if (field === 'auto_active') { + this.accountInfo.auto_active = oldValue === 1 ? 0 : 1 + } + }) + } finally { + this.switchLoading[field] = false + } + }, + + // 查看账户详情 + viewAccountDetail() { + if (this.accountInfo.id) { + this.$router.push({ + path: '/pla_account/pla_account_detail', + query: { id: this.accountInfo.id } + }) + } + }, + + // 获取平台颜色 + getPlatformColor(platformType) { + const colorMap = { + '1': 'blue', + '2': 'green' + } + return colorMap[platformType] || 'default' + }, + + // 获取平台文本 + getPlatformText(platformType) { + const textMap = { + '1': 'Boss直聘', + '2': '猎聘' + } + return textMap[platformType] || platformType || '-' } } } @@ -592,4 +880,43 @@ export default { font-weight: bold; color: #2d8cf0; } + +.account-info-card { + cursor: pointer; +} + +.account-info-card:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.account-info-item { + padding: 8px 0; +} + +.info-label { + font-size: 12px; + color: #999; + margin-bottom: 4px; +} + +.info-value { + font-size: 14px; + color: #333; + font-weight: 500; +} + +.account-switch-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 8px 0; +} + +.switch-label { + font-size: 12px; + color: #666; + margin-bottom: 8px; + text-align: center; +} diff --git a/api/controller_admin/pla_account.js b/api/controller_admin/pla_account.js index 8a37980..dedf016 100644 --- a/api/controller_admin/pla_account.js +++ b/api/controller_admin/pla_account.js @@ -147,6 +147,8 @@ module.exports = { 'POST /account/update': async (ctx) => { const body = ctx.getBody(); const { id, ...updateData } = body; + + await plaAccountService.updateAccount(id, updateData); return ctx.success({ message: '账号信息更新成功' }); }, diff --git a/api/middleware/schedule/config.js b/api/middleware/schedule/config.js index 4c19d0c..0591c85 100644 --- a/api/middleware/schedule/config.js +++ b/api/middleware/schedule/config.js @@ -1,5 +1,4 @@ const dayjs = require('dayjs'); -const config = require('../../../config/config'); /** * 调度系统配置中心 @@ -44,46 +43,16 @@ class ScheduleConfig { // 监控配置 this.monitoring = { - heartbeatTimeout: 3 * 60 * 1000, // 心跳超时:5分钟 - taskFailureRate: 0.5, // 任务失败率:50% - consecutiveFailures: 3, // 连续失败次数:3次 - alertCooldown: 5 * 60 * 1000, // 告警冷却:5分钟 + heartbeatTimeout: 3 * 60 * 1000, // 心跳超时:3分钟 offlineThreshold: 24 * 60 * 60 * 1000 // 离线设备清理:24小时 }; // 定时任务配置 this.schedules = { dailyReset: '0 0 * * *', // 每天凌晨重置统计 - jobFlowInterval: '0 */5 * * * *', // 每10秒执行一次找工作流程 monitoringInterval: '*/1 * * * *', // 监控检查间隔:1分钟 - autoDeliver: '0 */5 * * * *', // 自动投递任务:每5分钟执行一次 - // 监控检查间隔:1分钟 + autoDeliver: '0 */1 * * * *' // 自动投递任务:每5分钟执行一次 }; - - // 测试配置覆盖 - if (config.test) { - this.applyTestConfig(config.test); - } - } - - /** - * 应用测试配置 - * @param {object} testConfig - 测试配置 - */ - applyTestConfig(testConfig) { - if (testConfig.skipWorkStartHour) { - this.workHours.start = 0; - this.workHours.end = 24; - } - - - - // 测试模式下缩短所有间隔时间 - if (testConfig.fastMode) { - this.rateLimits.search = 10 * 1000; // 10秒 - this.rateLimits.apply = 5 * 1000; // 5秒 - this.rateLimits.chat = 2 * 1000; // 2秒 - } } /** diff --git a/api/middleware/schedule/scheduledJobs.js b/api/middleware/schedule/scheduledJobs.js index 2f8d212..d548f17 100644 --- a/api/middleware/schedule/scheduledJobs.js +++ b/api/middleware/schedule/scheduledJobs.js @@ -1,4 +1,5 @@ const node_schedule = require("node-schedule"); +const dayjs = require('dayjs'); const config = require('./config.js'); const deviceManager = require('./deviceManager.js'); const command = require('./command.js'); @@ -331,6 +332,8 @@ class ScheduledJobs { } }); + + if (!pla_users || pla_users.length === 0) { console.log('[自动投递] 没有启用且开启自动投递的账号'); return; @@ -338,11 +341,13 @@ class ScheduledJobs { console.log(`[自动投递] 找到 ${pla_users.length} 个可用账号`); + // 获取 task_status 模型用于查询上次投递时间 + const { task_status } = models; + // 为每个设备添加自动投递任务到队列 for (const pl_user of pla_users) { const userData = pl_user.toJSON(); - // 检查设备调度策略 const canExecute = deviceManager.canExecuteOperation(userData.sn_code, 'deliver'); if (!canExecute.allowed) { @@ -350,6 +355,44 @@ class ScheduledJobs { continue; } + // 获取投递配置,如果不存在则使用默认值 + const deliver_config = userData.deliver_config || { + deliver_interval: 30, + min_salary: 0, + max_salary: 0, + page_count: 3, + max_deliver: 10, + filter_keywords: [], + exclude_keywords: [] + }; + + // 检查投递间隔时间 + const deliver_interval = deliver_config.deliver_interval || 30; // 默认30分钟 + const interval_ms = deliver_interval * 60 * 1000; // 转换为毫秒 + + // 查询该账号最近一次成功完成的自动投递任务 + const lastDeliverTask = await task_status.findOne({ + where: { + sn_code: userData.sn_code, + taskType: 'auto_deliver', + status: 'completed' + }, + order: [['endTime', 'DESC']], + attributes: ['endTime'] + }); + + // 如果存在上次投递记录,检查是否已经过了间隔时间 + if (lastDeliverTask && lastDeliverTask.endTime) { + const lastDeliverTime = new Date(lastDeliverTask.endTime); + const elapsedTime = now.getTime() - lastDeliverTime.getTime(); + + if (elapsedTime < interval_ms) { + const remainingMinutes = Math.ceil((interval_ms - elapsedTime) / (60 * 1000)); + console.log(`[自动投递] 设备 ${userData.sn_code} 距离上次投递仅 ${Math.round(elapsedTime / (60 * 1000))} 分钟,还需等待 ${remainingMinutes} 分钟(间隔: ${deliver_interval} 分钟)`); + continue; + } + } + // 添加自动投递任务到队列 await this.taskQueue.addTask(userData.sn_code, { taskType: 'auto_deliver', @@ -357,19 +400,19 @@ class ScheduledJobs { taskParams: { keyword: userData.keyword || '', platform: userData.platform_type || 'boss', - pageCount: 3, // 默认值 - maxCount: 10, // 默认值 + pageCount: deliver_config.page_count || 3, + maxCount: deliver_config.max_deliver || 10, filterRules: { - minSalary: userData.min_salary || 0, - maxSalary: userData.max_salary || 0, - keywords: [], - excludeKeywords: [] + minSalary: deliver_config.min_salary || 0, + maxSalary: deliver_config.max_salary || 0, + keywords: deliver_config.filter_keywords || [], + excludeKeywords: deliver_config.exclude_keywords || [] } }, priority: config.getTaskPriority('auto_deliver') || 6 }); - console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'}`); + console.log(`[自动投递] 已为设备 ${userData.sn_code} 添加自动投递任务,关键词: ${userData.keyword || '默认'},投递间隔: ${deliver_interval} 分钟`); } console.log('[自动投递] 任务添加完成'); diff --git a/api/middleware/schedule/taskHandlers.js b/api/middleware/schedule/taskHandlers.js index c5a70f2..1fdf1ac 100644 --- a/api/middleware/schedule/taskHandlers.js +++ b/api/middleware/schedule/taskHandlers.js @@ -44,7 +44,7 @@ class TaskHandlers { async handleAutoDeliverTask(task) { const { sn_code, taskParams } = task; - const { keyword, platform, pageCount, maxCount } = taskParams; + const { keyword, platform, pageCount, maxCount, filterRules = {} } = taskParams; console.log(`[任务处理器] 自动投递任务 - 设备: ${sn_code}, 关键词: ${keyword}`); @@ -181,11 +181,22 @@ class TaskHandlers { // 5. 根据简历信息、职位类型配置和权重配置进行评分和过滤 const scoredJobs = []; - const excludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords + + // 合并排除关键词:从职位类型配置和任务参数中获取 + const jobTypeExcludeKeywords = jobTypeConfig && jobTypeConfig.excludeKeywords ? (typeof jobTypeConfig.excludeKeywords === 'string' ? JSON.parse(jobTypeConfig.excludeKeywords) : jobTypeConfig.excludeKeywords) : []; + const taskExcludeKeywords = filterRules.excludeKeywords || []; + const excludeKeywords = [...jobTypeExcludeKeywords, ...taskExcludeKeywords]; + + // 获取过滤关键词(用于优先匹配) + const filterKeywords = filterRules.keywords || []; + + // 获取薪资范围过滤 + const minSalary = filterRules.minSalary || 0; + const maxSalary = filterRules.maxSalary || 0; // 获取一个月内已投递的公司列表(用于过滤) const apply_records = db.getModel('apply_records'); @@ -209,6 +220,20 @@ class TaskHandlers { for (const job of pendingJobs) { const jobData = job.toJSON ? job.toJSON() : job; + // 薪资范围过滤 + if (minSalary > 0 || maxSalary > 0) { + const jobSalaryMin = jobData.salaryMin || 0; + const jobSalaryMax = jobData.salaryMax || 0; + + // 如果职位薪资范围与过滤范围没有交集,则跳过 + if (minSalary > 0 && jobSalaryMax > 0 && minSalary > jobSalaryMax) { + continue; + } + if (maxSalary > 0 && jobSalaryMin > 0 && maxSalary < jobSalaryMin) { + continue; + } + } + // 排除关键词过滤 if (Array.isArray(excludeKeywords) && excludeKeywords.length > 0) { const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase(); @@ -233,12 +258,28 @@ class TaskHandlers { priorityWeights ); + // 如果配置了过滤关键词,给包含这些关键词的职位加分 + let keywordBonus = 0; + if (Array.isArray(filterKeywords) && filterKeywords.length > 0) { + const jobText = `${jobData.jobTitle} ${jobData.companyName} ${jobData.jobDescription || ''}`.toLowerCase(); + const matchedKeywords = filterKeywords.filter(kw => jobText.includes(kw.toLowerCase())); + if (matchedKeywords.length > 0) { + // 每匹配一个关键词加5分,最多加20分 + keywordBonus = Math.min(matchedKeywords.length * 5, 20); + } + } + + const finalScore = scoreResult.totalScore + keywordBonus; + // 只保留总分 >= 60 的职位 - if (scoreResult.totalScore >= 60) { + if (finalScore >= 60) { scoredJobs.push({ ...jobData, - matchScore: scoreResult.totalScore, - scoreDetails: scoreResult.scores + matchScore: finalScore, + scoreDetails: { + ...scoreResult.scores, + keywordBonus: keywordBonus + } }); } } diff --git a/api/model/pla_account.js b/api/model/pla_account.js index 5953308..9aaa4ba 100644 --- a/api/model/pla_account.js +++ b/api/model/pla_account.js @@ -59,12 +59,31 @@ module.exports = (db) => { type: Sequelize.JSON(), allowNull: false, get: function () { - return JSON.parse(this.getDataValue('is_salary_priority')); + const value = this.getDataValue('is_salary_priority'); + if (!value) { + return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]; + } + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (e) { + return [{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]; + } + } + return value; }, set: function (value) { - this.setDataValue('is_salary_priority', JSON.stringify(value)); + if (value === null || value === undefined) { + this.setDataValue('is_salary_priority', JSON.stringify([{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}])); + } else if (typeof value === 'string') { + // 如果已经是字符串,直接使用 + this.setDataValue('is_salary_priority', value); + } else { + // 如果是对象/数组,序列化为字符串 + this.setDataValue('is_salary_priority', JSON.stringify(value)); + } }, - defaultValue: [ { "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20} ] + defaultValue: JSON.stringify([{ "key": "distance", "weight": 50 }, { "key": "salary", "weight": 20 }, { "key": "work_years", "weight": 10 }, { "key": "education", "weight": 20}]) }, @@ -97,17 +116,56 @@ module.exports = (db) => { allowNull: false, defaultValue: 0 }, - min_salary: { - comment: '最低薪资(单位:元)', - type: Sequelize.INTEGER, - allowNull: false, - defaultValue: 0 - }, - max_salary: { - comment: '最高薪资(单位:元)', - type: Sequelize.INTEGER, - allowNull: false, - defaultValue: 0 + // 自动投递配置(JSON格式,包含:deliver_interval-投递间隔分钟数, min_salary-最低薪资, max_salary-最高薪资, page_count-滚动获取职位列表次数, max_deliver-每次最多投递数量, filter_keywords-过滤关键词, exclude_keywords-排除关键词) + deliver_config: { + comment: '自动投递配置(JSON对象)', + type: Sequelize.JSON(), + allowNull: true, + get: function () { + const value = this.getDataValue('deliver_config'); + if (!value) return null; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (e) { + return null; + } + } + return value; + }, + set: function (value) { + if (value === null || value === undefined) { + this.setDataValue('deliver_config', null); + } else if (typeof value === 'string') { + // 如果已经是字符串,直接使用 + this.setDataValue('deliver_config', value); + } else { + // 如果是对象,序列化为字符串 + this.setDataValue('deliver_config', JSON.stringify(value)); + } + }, + // 默认值说明: + // deliver_interval: 30 - 投递间隔时间,单位:分钟,默认30分钟执行一次自动投递 + // min_salary: 0 - 最低薪资(单位:元),0表示不限制 + // max_salary: 0 - 最高薪资(单位:元),0表示不限制 + // page_count: 3 - 滚动获取职位列表次数,默认3次 + // max_deliver: 10 - 每次最多投递数量,默认10个 + // filter_keywords: [] - 过滤关键词数组,包含这些关键词的职位会被优先考虑 + // exclude_keywords: [] - 排除关键词数组,包含这些关键词的职位会被排除 + defaultValue: JSON.stringify({ + deliver_interval: 30, + min_salary: 0, + max_salary: 0, + page_count: 3, + max_deliver: 10, + time_range: { + start_time: '09:00', + end_time: '18:00', + workdays_only: 1 + }, + filter_keywords: [], + exclude_keywords: [] + }) }, // 自动沟通相关配置 auto_chat: { @@ -116,18 +174,52 @@ module.exports = (db) => { allowNull: false, defaultValue: 0 }, - chat_interval: { - comment: '沟通间隔(单位:分钟)', - type: Sequelize.INTEGER, - allowNull: false, - defaultValue: 30 - }, - auto_reply: { - comment: '自动回复开关', - type: Sequelize.TINYINT(1), - allowNull: false, - defaultValue: 0 + // 自动沟通配置(JSON格式,包含:chat_interval-沟通间隔分钟数, is_chat_outsourcing-是否沟通外包岗位, time_range-沟通时间段) + chat_strategy: { + comment: '自动沟通策略配置(JSON对象)', + type: Sequelize.JSON(), + allowNull: true, + get: function () { + const value = this.getDataValue('chat_strategy'); + if (!value) return null; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (e) { + return null; + } + } + return value; + }, + set: function (value) { + if (value === null || value === undefined) { + this.setDataValue('chat_strategy', null); + } else if (typeof value === 'string') { + // 如果已经是字符串,直接使用 + this.setDataValue('chat_strategy', value); + } else { + // 如果是对象,序列化为字符串 + this.setDataValue('chat_strategy', JSON.stringify(value)); + } + }, + // 默认值说明: + // chat_interval: 30 - 沟通间隔时间,单位:分钟,默认30分钟执行一次自动沟通 + // is_chat_outsourcing: 0 - 是否沟通外包岗位,0=不沟通外包岗位,1=沟通外包岗位 + // time_range: 沟通时间段配置 + // - start_time: 开始时间,格式:HH:mm,默认 "09:00" + // - end_time: 结束时间,格式:HH:mm,默认 "18:00" + // - workdays_only: 是否仅工作日,0=包含周末,1=仅工作日,默认1 + defaultValue: JSON.stringify({ + chat_interval: 30, + is_chat_outsourcing: 0, + time_range: { + start_time: '09:00', + end_time: '18:00', + workdays_only: 1 + } + }) }, + // 自动活跃相关配置 auto_active: { comment: '自动活跃开关', @@ -135,18 +227,47 @@ module.exports = (db) => { allowNull: false, defaultValue: 0 }, - // 是否沟通外包岗位 - is_chat_outsourcing: { - comment: '是否沟通外包岗位', - type: Sequelize.TINYINT(1), - allowNull: false, - defaultValue: 0 + // 自动活跃配置(JSON格式,包含:active_interval-活跃间隔分钟数, actions-活跃动作列表) + active_actions: { + comment: '自动活跃动作配置(JSON对象)', + type: Sequelize.JSON(), + allowNull: true, + get: function () { + const value = this.getDataValue('active_actions'); + if (!value) return null; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch (e) { + return null; + } + } + return value; + }, + set: function (value) { + if (value === null || value === undefined) { + this.setDataValue('active_actions', null); + } else if (typeof value === 'string') { + // 如果已经是字符串,直接使用 + this.setDataValue('active_actions', value); + } else { + // 如果是对象,序列化为字符串 + this.setDataValue('active_actions', JSON.stringify(value)); + } + }, + // 默认值说明: + // active_interval: 60 - 活跃间隔时间,单位:分钟,默认60分钟执行一次活跃动作 + // actions: [] - 活跃动作列表,数组格式,可包含多个活跃动作配置对象 + defaultValue: JSON.stringify({ + active_interval: 60, + actions: [] + }) }, }); - //pla_account.sync({ force: true }); + // pla_account.sync({ force: true }); return pla_account diff --git a/app.js b/app.js index 5904385..0dba640 100644 --- a/app.js +++ b/app.js @@ -36,7 +36,7 @@ async function startApp() { // 启动调度系统 - await schedule.init(); + // await schedule.init(); // 优雅关闭处理 process.on('SIGINT', async () => {
当前执行中的任务
暂无执行中的任务
加载中...