From 61698639ef06b86413cd199d7490b6e441eaf5bb Mon Sep 17 00:00:00 2001 From: KQL Date: Mon, 22 Dec 2025 15:40:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=A4=9A=E5=A4=9A?= =?UTF-8?q?=E7=95=85=E8=81=8C=E5=B0=B1=E4=B8=9A=E6=9C=8D=E5=8A=A1=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E6=A0=B8=E5=BF=83=E5=8A=9F=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: - ✅ 完成主题配色从暗色到亮蓝白配色的全面转换 - ✅ 实现高薪岗位页面及后端API集成 - ✅ 完成登录注册页面及认证系统 - ✅ 实现预招录确认功能 - ✅ 添加数据库管理和维护工具脚本 - ✅ 优化错误处理和用户体验 核心功能: 1. 首页 (index.html) - 3D地球、专业分类、过渡岗位 2. 高薪岗位页面 (high.html) - 岗位详情、预招录确认、成功案例 3. 登录注册 (auth.html) - 用户认证、专业分类选择 4. 后端API - RESTful接口,JWT认证,MySQL数据库 技术栈: - 前端:Three.js, GSAP, 原生JavaScript - 后端:Node.js, Express, MySQL - 认证:JWT, bcrypt - 样式:自定义CSS,响应式设计 数据库工具: - kill-by-ids.js - 批量终止MySQL进程 - unlock-all-tables.js - 解锁数据库表 - init-db.js - 初始化数据库 - 其他管理脚本 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 5 + 3.html | 842 ++++++ api/controllers/applicationController.js | 272 ++ api/controllers/authController.js | 337 +++ api/controllers/highSalaryJobController.js | 251 ++ api/controllers/successStoryController.js | 224 ++ api/controllers/trainingUnitController.js | 229 ++ .../transitionProgressController.js | 270 ++ api/routes/applications.js | 20 + api/routes/auth.js | 18 + api/routes/highSalaryJobs.js | 19 + api/routes/successStories.js | 18 + api/routes/trainingUnits.js | 18 + api/routes/transitionProgress.js | 19 + auth.html | 387 +++ config/database.js | 32 + css/components.css | 53 +- css/layout.css | 10 +- css/website.css | 4 +- high copy.html | 1719 +++++++++++++ high.html | 1080 ++++++++ index copy.html | 1600 ++++++++++++ index.html | 2267 ++++++++++------- js/auth.js | 350 +++ js/ui/ListInterface.js | 12 +- js/ui/MapInterface.js | 26 +- js/utils/auth-helper.js | 220 ++ js/website.js | 16 +- middleware/auth.js | 76 + package-lock.json | 1597 ++++++++++++ package.json | 32 + scripts/add-professional-category.js | 110 + scripts/add-professional-category.sql | 13 + scripts/create-admin.js | 87 + scripts/create-database.sql | 27 + scripts/database-setup.sql | 143 ++ scripts/fix-database-locks.js | 117 + scripts/fix-locked-transaction.js | 88 + scripts/grant-permissions.sql | 18 + scripts/init-db.js | 209 ++ scripts/init-high-page-data.sql | 341 +++ scripts/kill-by-ids.js | 62 + scripts/kill-locked-processes.js | 136 + scripts/quick-kill-all.js | 66 + scripts/run-init-sql.js | 46 + scripts/simple-test.js | 116 + scripts/test-permissions.js | 92 + scripts/test-training-confirmation.js | 125 + scripts/unlock-all-tables.js | 44 + scripts/unlock-tables.js | 68 + server.js | 200 +- 专业大类.json | 78 + 启动服务器.bat | 51 - 启动服务器.sh | 72 - 快捷部署说明.md | 144 -- 55 files changed, 13178 insertions(+), 1298 deletions(-) create mode 100644 3.html create mode 100644 api/controllers/applicationController.js create mode 100644 api/controllers/authController.js create mode 100644 api/controllers/highSalaryJobController.js create mode 100644 api/controllers/successStoryController.js create mode 100644 api/controllers/trainingUnitController.js create mode 100644 api/controllers/transitionProgressController.js create mode 100644 api/routes/applications.js create mode 100644 api/routes/auth.js create mode 100644 api/routes/highSalaryJobs.js create mode 100644 api/routes/successStories.js create mode 100644 api/routes/trainingUnits.js create mode 100644 api/routes/transitionProgress.js create mode 100644 auth.html create mode 100644 config/database.js create mode 100644 high copy.html create mode 100644 high.html create mode 100644 index copy.html create mode 100644 js/auth.js create mode 100644 js/utils/auth-helper.js create mode 100644 middleware/auth.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/add-professional-category.js create mode 100644 scripts/add-professional-category.sql create mode 100644 scripts/create-admin.js create mode 100644 scripts/create-database.sql create mode 100644 scripts/database-setup.sql create mode 100644 scripts/fix-database-locks.js create mode 100644 scripts/fix-locked-transaction.js create mode 100644 scripts/grant-permissions.sql create mode 100644 scripts/init-db.js create mode 100644 scripts/init-high-page-data.sql create mode 100644 scripts/kill-by-ids.js create mode 100644 scripts/kill-locked-processes.js create mode 100644 scripts/quick-kill-all.js create mode 100644 scripts/run-init-sql.js create mode 100644 scripts/simple-test.js create mode 100644 scripts/test-permissions.js create mode 100644 scripts/test-training-confirmation.js create mode 100644 scripts/unlock-all-tables.js create mode 100644 scripts/unlock-tables.js create mode 100644 专业大类.json delete mode 100644 启动服务器.bat delete mode 100755 启动服务器.sh delete mode 100644 快捷部署说明.md diff --git a/.gitignore b/.gitignore index 2e71b19..05e2e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ # Node node_modules/ +# Environment variables (contains sensitive data) +.env +.env.local +.env.*.local + # Test folders (可以根据需要调整) 测试/ 测试2/ diff --git a/3.html b/3.html new file mode 100644 index 0000000..398d6a4 --- /dev/null +++ b/3.html @@ -0,0 +1,842 @@ + + + + + 岗位装配中心 - 详情增强版 + + + + + + +
+ +
+ +
+
+
+ +
+
我的意向过渡岗位
+ INTENDED POSITIONS +
+
+
+ 已选: 0 / 3 +
+
+
+ +
+ +
+
+
+ +
+
过渡岗位确认
+ FINAL LOCK +
+
+
+
+ +
+ +
+ +
+
+
岗位资源库
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/api/controllers/applicationController.js b/api/controllers/applicationController.js new file mode 100644 index 0000000..858ce0a --- /dev/null +++ b/api/controllers/applicationController.js @@ -0,0 +1,272 @@ +/** + * 投递记录控制器 + */ + +const db = require('../../config/database'); + +/** + * 创建投递记录 + */ +exports.createApplication = async (req, res) => { + try { + const userId = req.user.id; + const { + job_type, job_name, company_name, company_short_name, + city, province, segment_name, application_data + } = req.body; + + // 验证必填字段 + if (!job_type || !job_name || !company_name) { + return res.status(400).json({ + success: false, + message: '岗位类型、岗位名称和企业名称不能为空' + }); + } + + // 验证job_type + if (!['transition', 'referral'].includes(job_type)) { + return res.status(400).json({ + success: false, + message: '无效的岗位类型' + }); + } + + // 检查是否已投递过相同岗位 + const [existing] = await db.query(` + SELECT id FROM job_applications + WHERE user_id = ? AND job_name = ? AND company_name = ? AND status != 'withdrawn' + `, [userId, job_name, company_name]); + + if (existing.length > 0) { + return res.status(400).json({ + success: false, + message: '您已经投递过该岗位' + }); + } + + // 插入投递记录 + const [result] = await db.query(` + INSERT INTO job_applications ( + user_id, job_type, job_name, company_name, company_short_name, + city, province, segment_name, status, application_data + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?) + `, [ + userId, job_type, job_name, company_name, company_short_name || null, + city || null, province || null, segment_name || null, + application_data ? JSON.stringify(application_data) : null + ]); + + res.status(201).json({ + success: true, + message: '投递成功', + data: { + id: result.insertId + } + }); + + } catch (error) { + console.error('创建投递记录错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 获取投递记录列表 + */ +exports.getApplications = async (req, res) => { + try { + const userId = req.user.id; + const { job_type, status, page = 1, limit = 20 } = req.query; + + // 构建查询条件 + let whereClause = 'user_id = ?'; + const params = [userId]; + + if (job_type) { + whereClause += ' AND job_type = ?'; + params.push(job_type); + } + + if (status) { + whereClause += ' AND status = ?'; + params.push(status); + } + + // 计算分页 + const offset = (page - 1) * limit; + + // 查询总数 + const [countResult] = await db.query( + `SELECT COUNT(*) as total FROM job_applications WHERE ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 查询列表 + const [applications] = await db.query(` + SELECT + id, job_type, job_name, company_name, company_short_name, + city, province, segment_name, status, applied_at, updated_at, notes + FROM job_applications + WHERE ${whereClause} + ORDER BY applied_at DESC + LIMIT ? OFFSET ? + `, [...params, parseInt(limit), parseInt(offset)]); + + res.json({ + success: true, + data: { + list: applications, + pagination: { + page: parseInt(page), + limit: parseInt(limit), + total, + totalPages: Math.ceil(total / limit) + } + } + }); + + } catch (error) { + console.error('获取投递记录错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 获取单个投递记录详情 + */ +exports.getApplicationDetail = async (req, res) => { + try { + const userId = req.user.id; + const { id } = req.params; + + const [applications] = await db.query(` + SELECT * FROM job_applications + WHERE id = ? AND user_id = ? + `, [id, userId]); + + if (applications.length === 0) { + return res.status(404).json({ + success: false, + message: '投递记录不存在' + }); + } + + const application = applications[0]; + + // 解析JSON字段 + if (application.application_data) { + try { + application.application_data = JSON.parse(application.application_data); + } catch (e) { + application.application_data = null; + } + } + + res.json({ + success: true, + data: application + }); + + } catch (error) { + console.error('获取投递详情错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 撤回投递 + */ +exports.withdrawApplication = async (req, res) => { + try { + const userId = req.user.id; + const { id } = req.params; + + // 检查投递记录是否存在且属于当前用户 + const [applications] = await db.query(` + SELECT id, status FROM job_applications + WHERE id = ? AND user_id = ? + `, [id, userId]); + + if (applications.length === 0) { + return res.status(404).json({ + success: false, + message: '投递记录不存在' + }); + } + + const application = applications[0]; + + // 检查状态是否允许撤回 + if (['offered', 'rejected', 'withdrawn'].includes(application.status)) { + return res.status(400).json({ + success: false, + message: '该投递记录无法撤回' + }); + } + + // 更新状态为撤回 + await db.query(` + UPDATE job_applications + SET status = 'withdrawn', updated_at = NOW() + WHERE id = ? + `, [id]); + + res.json({ + success: true, + message: '撤回成功' + }); + + } catch (error) { + console.error('撤回投递错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 获取投递统计 + */ +exports.getStatistics = async (req, res) => { + try { + const userId = req.user.id; + + // 统计各状态的投递数量 + const [stats] = await db.query(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN job_type = 'transition' THEN 1 ELSE 0 END) as transition_count, + SUM(CASE WHEN job_type = 'referral' THEN 1 ELSE 0 END) as referral_count, + SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count, + SUM(CASE WHEN status = 'reviewing' THEN 1 ELSE 0 END) as reviewing_count, + SUM(CASE WHEN status = 'interviewed' THEN 1 ELSE 0 END) as interviewed_count, + SUM(CASE WHEN status = 'offered' THEN 1 ELSE 0 END) as offered_count, + SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END) as rejected_count + FROM job_applications + WHERE user_id = ? + `, [userId]); + + res.json({ + success: true, + data: stats[0] + }); + + } catch (error) { + console.error('获取统计错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; diff --git a/api/controllers/authController.js b/api/controllers/authController.js new file mode 100644 index 0000000..d3c9e26 --- /dev/null +++ b/api/controllers/authController.js @@ -0,0 +1,337 @@ +/** + * 用户认证控制器 + */ + +const bcrypt = require('bcryptjs'); +const jwt = require('jsonwebtoken'); +const db = require('../../config/database'); + +const JWT_SECRET = process.env.JWT_SECRET || 'duoduo-career-jwt-secret-key-2024'; +const JWT_EXPIRE = process.env.JWT_EXPIRE || '7d'; + +/** + * 用户注册 + */ +exports.register = async (req, res) => { + try { + const { username, password, email, phone, professional_category_code, professional_category } = req.body; + + // 验证必填字段 + if (!username || !password) { + return res.status(400).json({ + success: false, + message: '真实姓名和密码不能为空' + }); + } + + // 验证真实姓名长度 + if (username.length < 2 || username.length > 10) { + return res.status(400).json({ + success: false, + message: '姓名长度必须在2-10个字符之间' + }); + } + + // 验证密码长度 + if (password.length < 6) { + return res.status(400).json({ + success: false, + message: '密码长度至少为6个字符' + }); + } + + // 验证手机号必填 + if (!phone) { + return res.status(400).json({ + success: false, + message: '手机号码不能为空' + }); + } + + // 检查手机号是否已被使用(手机号不允许重复) + const [existingPhones] = await db.query( + 'SELECT id FROM users WHERE phone = ?', + [phone] + ); + if (existingPhones.length > 0) { + return res.status(400).json({ + success: false, + message: '该手机号已被注册' + }); + } + + // 如果提供了邮箱,检查是否已被使用 + if (email) { + const [existingEmails] = await db.query( + 'SELECT id FROM users WHERE email = ?', + [email] + ); + if (existingEmails.length > 0) { + return res.status(400).json({ + success: false, + message: '邮箱已被使用' + }); + } + } + + // 加密密码 + const passwordHash = await bcrypt.hash(password, 10); + + // 插入用户 + const [result] = await db.query( + 'INSERT INTO users (username, password_hash, email, phone, role, status) VALUES (?, ?, ?, ?, ?, ?)', + [username, passwordHash, email || null, phone || null, 'user', 'active'] + ); + + // 创建用户资料记录 + await db.query( + `INSERT INTO user_profiles ( + user_id, + professional_category_code, + professional_category + ) VALUES (?, ?, ?)`, + [ + result.insertId, + professional_category_code || null, + professional_category || null + ] + ); + + res.status(201).json({ + success: true, + message: '注册成功', + data: { + id: result.insertId, + username + } + }); + + } catch (error) { + console.error('注册错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误,请稍后重试' + }); + } +}; + +/** + * 用户登录 + */ +exports.login = async (req, res) => { + try { + const { username, password } = req.body; + + // 验证必填字段 + if (!username || !password) { + return res.status(400).json({ + success: false, + message: '用户名和密码不能为空' + }); + } + + // 查询用户 + const [users] = await db.query(` + SELECT + u.id, u.username, u.password_hash, u.email, u.phone, u.role, u.status, + p.professional_category_code, + p.professional_category + FROM users u + LEFT JOIN user_profiles p ON u.id = p.user_id + WHERE u.username = ? + `, [username]); + + if (users.length === 0) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + const user = users[0]; + + // 检查账号状态 + if (user.status === 'banned') { + return res.status(403).json({ + success: false, + message: '账号已被封禁' + }); + } + + if (user.status === 'inactive') { + return res.status(403).json({ + success: false, + message: '账号未激活' + }); + } + + // 验证密码 + const isPasswordValid = await bcrypt.compare(password, user.password_hash); + + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + message: '用户名或密码错误' + }); + } + + // 尝试更新最后登录时间(非阻塞) + try { + await db.query( + 'UPDATE users SET last_login = NOW() WHERE id = ?', + [user.id] + ); + } catch (updateError) { + // 如果更新失败(如数据库锁定),记录错误但不影响登录 + console.warn('更新登录时间失败:', updateError.message); + } + + // 生成JWT + const token = jwt.sign( + { + id: user.id, + username: user.username, + role: user.role + }, + JWT_SECRET, + { expiresIn: JWT_EXPIRE } + ); + + res.json({ + success: true, + message: '登录成功', + data: { + token, + user: { + id: user.id, + username: user.username, + email: user.email, + phone: user.phone, + role: user.role, + professional_category_code: user.professional_category_code, + professional_category: user.professional_category + } + } + }); + + } catch (error) { + console.error('登录错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误,请稍后重试' + }); + } +}; + +/** + * 获取当前用户信息 + */ +exports.getCurrentUser = async (req, res) => { + try { + const userId = req.user.id; + + // 查询用户基本信息和资料 + const [users] = await db.query(` + SELECT + u.id, u.username, u.email, u.phone, u.role, u.status, u.created_at, u.last_login, + p.real_name, p.gender, p.birth_date, p.education, p.major, p.school, + p.graduation_year, p.city, p.avatar_url, p.self_intro, p.skills, + p.professional_category_code, + p.professional_category + FROM users u + LEFT JOIN user_profiles p ON u.id = p.user_id + WHERE u.id = ? + `, [userId]); + + if (users.length === 0) { + return res.status(404).json({ + success: false, + message: '用户不存在' + }); + } + + const user = users[0]; + + // 解析JSON字段 + if (user.skills) { + try { + user.skills = JSON.parse(user.skills); + } catch (e) { + user.skills = []; + } + } + + res.json({ + success: true, + data: user + }); + + } catch (error) { + console.error('获取用户信息错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; + +/** + * 更新用户资料 + */ +exports.updateProfile = async (req, res) => { + try { + const userId = req.user.id; + const { + real_name, gender, birth_date, education, major, school, + graduation_year, city, address, avatar_url, self_intro, skills, + professional_category_code, + professional_category + } = req.body; + + // 构建更新字段 + const updates = {}; + if (real_name !== undefined) updates.real_name = real_name; + if (gender !== undefined) updates.gender = gender; + if (birth_date !== undefined) updates.birth_date = birth_date; + if (education !== undefined) updates.education = education; + if (major !== undefined) updates.major = major; + if (school !== undefined) updates.school = school; + if (graduation_year !== undefined) updates.graduation_year = graduation_year; + if (city !== undefined) updates.city = city; + if (address !== undefined) updates.address = address; + if (avatar_url !== undefined) updates.avatar_url = avatar_url; + if (self_intro !== undefined) updates.self_intro = self_intro; + if (skills !== undefined) updates.skills = JSON.stringify(skills); + if (professional_category_code !== undefined) updates.professional_category_code = professional_category_code; + if (professional_category !== undefined) updates.professional_category = professional_category; + + if (Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + message: '没有要更新的字段' + }); + } + + // 构建SQL + const fields = Object.keys(updates).map(key => `${key} = ?`).join(', '); + const values = Object.values(updates); + values.push(userId); + + await db.query( + `UPDATE user_profiles SET ${fields}, updated_at = NOW() WHERE user_id = ?`, + values + ); + + res.json({ + success: true, + message: '资料更新成功' + }); + + } catch (error) { + console.error('更新资料错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误' + }); + } +}; diff --git a/api/controllers/highSalaryJobController.js b/api/controllers/highSalaryJobController.js new file mode 100644 index 0000000..a856993 --- /dev/null +++ b/api/controllers/highSalaryJobController.js @@ -0,0 +1,251 @@ +/** + * 高薪岗位控制器 + * 基于high.html的最新数据结构 + */ + +const db = require('../../config/database'); + +/** + * 获取岗位列表 + * GET /api/high-salary-jobs + * 支持分页、筛选、搜索 + */ +exports.getList = async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const offset = (page - 1) * limit; + const location = req.query.location; + const keyword = req.query.keyword; + + // 构建查询条件 + let whereClause = 'WHERE is_active = 1'; + const params = []; + + if (location) { + whereClause += ' AND location = ?'; + params.push(location); + } + + if (keyword) { + whereClause += ' AND (title LIKE ? OR company LIKE ?)'; + params.push(`%${keyword}%`, `%${keyword}%`); + } + + // 查询总数 + const [countResult] = await db.query( + `SELECT COUNT(*) as total FROM high_salary_jobs ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 查询岗位列表 + const [jobs] = await db.query( + `SELECT * FROM high_salary_jobs ${whereClause} ORDER BY display_order ASC, id DESC LIMIT ? OFFSET ?`, + [...params, limit, offset] + ); + + return res.json({ + success: true, + data: { + jobs, + total, + page, + limit, + totalPages: Math.ceil(total / limit) + } + }); + } catch (error) { + console.error('获取岗位列表失败:', error); + return res.status(500).json({ + success: false, + message: '获取岗位列表失败', + error: error.message + }); + } +}; + +/** + * 获取岗位详情 + * GET /api/high-salary-jobs/:id + */ +exports.getById = async (req, res) => { + try { + const { id } = req.params; + + const [jobs] = await db.query( + 'SELECT * FROM high_salary_jobs WHERE id = ? AND is_active = 1', + [id] + ); + + if (jobs.length === 0) { + return res.status(404).json({ + success: false, + message: '岗位不存在' + }); + } + + return res.json({ + success: true, + data: jobs[0] + }); + } catch (error) { + console.error('获取岗位详情失败:', error); + return res.status(500).json({ + success: false, + message: '获取岗位详情失败', + error: error.message + }); + } +}; + +/** + * 创建岗位(管理员) + * POST /api/high-salary-jobs + */ +exports.create = async (req, res) => { + try { + const { + title, + salary, + company, + location, + quota, + tags, + requirements, + description, + icon, + display_order + } = req.body; + + // 验证必填字段 + if (!title || !salary || !company || !location || quota === undefined) { + return res.status(400).json({ + success: false, + message: '缺少必填字段' + }); + } + + const [result] = await db.query( + `INSERT INTO high_salary_jobs + (title, salary, company, location, quota, tags, requirements, description, icon, display_order) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + title, + salary, + company, + location, + quota, + JSON.stringify(tags || []), + requirements, + description, + icon || 'fa-microchip', + display_order || 0 + ] + ); + + res.status(201).json({ + success: true, + message: '岗位创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建岗位失败:', error); + res.status(500).json({ + success: false, + message: '创建岗位失败', + error: error.message + }); + } +}; + +/** + * 更新岗位(管理员) + * PUT /api/high-salary-jobs/:id + */ +exports.update = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + // 检查岗位是否存在 + const [existing] = await db.query('SELECT id FROM high_salary_jobs WHERE id = ?', [id]); + if (existing.length === 0) { + return res.status(404).json({ + success: false, + message: '岗位不存在' + }); + } + + const updates = []; + const params = []; + + const allowedFields = ['title', 'salary', 'company', 'location', 'quota', 'tags', 'requirements', 'description', 'icon', 'display_order', 'is_active']; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + updates.push(`${field} = ?`); + params.push(field === 'tags' ? JSON.stringify(updateData[field]) : updateData[field]); + } + } + + if (updates.length === 0) { + return res.status(400).json({ + success: false, + message: '没有要更新的字段' + }); + } + + params.push(id); + await db.query( + `UPDATE high_salary_jobs SET ${updates.join(', ')} WHERE id = ?`, + params + ); + + res.json({ + success: true, + message: '岗位更新成功' + }); + } catch (error) { + console.error('更新岗位失败:', error); + res.status(500).json({ + success: false, + message: '更新岗位失败', + error: error.message + }); + } +}; + +/** + * 删除岗位(软删除) + * DELETE /api/high-salary-jobs/:id + */ +exports.delete = async (req, res) => { + try { + const { id } = req.params; + + const [result] = await db.query( + 'UPDATE high_salary_jobs SET is_active = 0 WHERE id = ?', + [id] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ + success: false, + message: '岗位不存在' + }); + } + + res.json({ + success: true, + message: '岗位删除成功' + }); + } catch (error) { + console.error('删除岗位失败:', error); + res.status(500).json({ + success: false, + message: '删除岗位失败', + error: error.message + }); + } +}; diff --git a/api/controllers/successStoryController.js b/api/controllers/successStoryController.js new file mode 100644 index 0000000..e5752fe --- /dev/null +++ b/api/controllers/successStoryController.js @@ -0,0 +1,224 @@ +/** + * 成功案例控制器 + * 基于high.html的最新数据结构 (三轨道弹幕系统) + */ + +const db = require('../../config/database'); + +/** + * 获取成功案例列表 (按轨道分组) + * GET /api/success-stories + * 支持可选的track_type筛选 + */ +exports.getList = async (req, res) => { + try { + const trackType = req.query.track_type; + + // 构建查询条件 + let whereClause = 'WHERE is_active = 1'; + const params = []; + + if (trackType && ['fast', 'slow', 'medium'].includes(trackType)) { + whereClause += ' AND track_type = ?'; + params.push(trackType); + } + + const [stories] = await db.query( + `SELECT + id, + name, + job, + company, + salary, + avatar_color, + track_type + FROM success_stories + ${whereClause} + ORDER BY display_order ASC, id ASC`, + params + ); + + // 按轨道分组 + const grouped = { + fast: stories.filter(s => s.track_type === 'fast'), + slow: stories.filter(s => s.track_type === 'slow'), + medium: stories.filter(s => s.track_type === 'medium') + }; + + return res.json({ + success: true, + data: grouped + }); + } catch (error) { + console.error('获取成功案例失败:', error); + return res.status(500).json({ + success: false, + message: '获取成功案例失败', + error: error.message + }); + } +}; + +/** + * 创建成功案例 (管理员) + * POST /api/success-stories + */ +exports.create = async (req, res) => { + try { + const { + name, + job, + company, + salary, + avatar_color, + track_type, + display_order + } = req.body; + + // 验证必填字段 + if (!name || !job || !company || !salary) { + return res.status(400).json({ + success: false, + message: '缺少必填字段 (name, job, company, salary)' + }); + } + + // 验证track_type + if (track_type && !['fast', 'slow', 'medium'].includes(track_type)) { + return res.status(400).json({ + success: false, + message: 'track_type 必须是 fast, slow 或 medium' + }); + } + + const [result] = await db.query( + `INSERT INTO success_stories + (name, job, company, salary, avatar_color, track_type, display_order) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + name, + job, + company, + salary, + avatar_color || '#2563eb', + track_type || 'fast', + display_order || 0 + ] + ); + + return res.status(201).json({ + success: true, + message: '成功案例创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建成功案例失败:', error); + return res.status(500).json({ + success: false, + message: '创建成功案例失败', + error: error.message + }); + } +}; + +/** + * 更新成功案例 (管理员) + * PUT /api/success-stories/:id + */ +exports.update = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + // 检查案例是否存在 + const [existing] = await db.query( + 'SELECT id FROM success_stories WHERE id = ?', + [id] + ); + + if (existing.length === 0) { + return res.status(404).json({ + success: false, + message: '成功案例不存在' + }); + } + + const updates = []; + const params = []; + + const allowedFields = ['name', 'job', 'company', 'salary', 'avatar_color', 'track_type', 'display_order', 'is_active']; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + // 验证track_type + if (field === 'track_type' && !['fast', 'slow', 'medium'].includes(updateData[field])) { + return res.status(400).json({ + success: false, + message: 'track_type 必须是 fast, slow 或 medium' + }); + } + updates.push(`${field} = ?`); + params.push(updateData[field]); + } + } + + if (updates.length === 0) { + return res.status(400).json({ + success: false, + message: '没有要更新的字段' + }); + } + + params.push(id); + await db.query( + `UPDATE success_stories SET ${updates.join(', ')} WHERE id = ?`, + params + ); + + return res.json({ + success: true, + message: '成功案例更新成功' + }); + } catch (error) { + console.error('更新成功案例失败:', error); + return res.status(500).json({ + success: false, + message: '更新成功案例失败', + error: error.message + }); + } +}; + +/** + * 删除成功案例 (软删除) + * DELETE /api/success-stories/:id + */ +exports.delete = async (req, res) => { + try { + const { id } = req.params; + + const [result] = await db.query( + 'UPDATE success_stories SET is_active = 0 WHERE id = ?', + [id] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ + success: false, + message: '成功案例不存在' + }); + } + + return res.json({ + success: true, + message: '成功案例删除成功' + }); + } catch (error) { + console.error('删除成功案例失败:', error); + return res.status(500).json({ + success: false, + message: '删除成功案例失败', + error: error.message + }); + } +}; diff --git a/api/controllers/trainingUnitController.js b/api/controllers/trainingUnitController.js new file mode 100644 index 0000000..96bea1b --- /dev/null +++ b/api/controllers/trainingUnitController.js @@ -0,0 +1,229 @@ +/** + * 培训单元控制器 + * 基于high.html的最新数据结构 + */ + +const db = require('../../config/database'); + +/** + * 获取所有培训单元 + * GET /api/training-units + * 支持optionalAuth + */ +exports.getList = async (req, res) => { + try { + const userId = req.user ? req.user.id : null; + + let query, params; + + if (userId) { + // 已登录:查询确认状态 + query = ` + SELECT + tu.*, + tc.id as confirmation_id, + tc.confirmed_at + FROM training_units tu + LEFT JOIN training_confirmations tc ON tu.id = tc.training_unit_id AND tc.user_id = ? + ORDER BY tu.display_order ASC, tu.start_date ASC + `; + params = [userId]; + } else { + // 未登录:仅返回单元列表 + query = ` + SELECT * FROM training_units + ORDER BY display_order ASC, start_date ASC + `; + params = []; + } + + const [units] = await db.query(query, params); + + // 格式化数据 + const formattedUnits = units.map(unit => ({ + id: unit.id, + title: unit.title, + start_date: unit.start_date, + related_positions: unit.related_positions, + related_companies: unit.related_companies, + status: unit.status, + is_confirmed: !!unit.confirmation_id, + confirmed_at: unit.confirmed_at || null + })); + + return res.json({ + success: true, + data: formattedUnits + }); + } catch (error) { + console.error('获取培训单元失败:', error); + return res.status(500).json({ + success: false, + message: '获取培训单元失败', + error: error.message + }); + } +}; + +/** + * 确认预招录 + * POST /api/training-units/:id/confirm + * 需要登录 + */ +exports.confirm = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.id; + + // 检查单元是否存在 + const [units] = await db.query( + 'SELECT id, title FROM training_units WHERE id = ?', + [id] + ); + + if (units.length === 0) { + return res.status(404).json({ + success: false, + message: '培训单元不存在' + }); + } + + // 检查是否已确认 + const [existing] = await db.query( + 'SELECT id FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [userId, id] + ); + + if (existing.length > 0) { + return res.status(400).json({ + success: false, + message: '您已确认过该单元' + }); + } + + // 插入确认记录(设置较短的超时时间) + try { + await db.query( + 'INSERT INTO training_confirmations (user_id, training_unit_id) VALUES (?, ?)', + [userId, id] + ); + } catch (insertError) { + // 特殊处理数据库锁定错误 + if (insertError.code === 'ER_LOCK_WAIT_TIMEOUT') { + return res.status(503).json({ + success: false, + message: '系统繁忙,请稍后再试', + error: '数据库暂时无法处理请求' + }); + } + throw insertError; + } + + return res.json({ + success: true, + message: '确认成功' + }); + } catch (error) { + // 处理唯一键冲突 + if (error.code === 'ER_DUP_ENTRY') { + return res.status(400).json({ + success: false, + message: '您已确认过该单元' + }); + } + + // 处理数据库锁定错误 + if (error.code === 'ER_LOCK_WAIT_TIMEOUT') { + console.error('数据库锁定:', error.message); + return res.status(503).json({ + success: false, + message: '系统繁忙,请稍后再试', + error: '数据库暂时无法处理请求' + }); + } + + console.error('确认预招录失败:', error); + return res.status(500).json({ + success: false, + message: '确认失败,请稍后重试' + }); + } +}; + +/** + * 取消预招录 + * DELETE /api/training-units/:id/confirm + * 需要登录 + */ +exports.cancelConfirm = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.id; + + const [result] = await db.query( + 'DELETE FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [userId, id] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ + success: false, + message: '未找到确认记录' + }); + } + + return res.json({ + success: true, + message: '取消成功' + }); + } catch (error) { + console.error('取消预招录失败:', error); + return res.status(500).json({ + success: false, + message: '取消预招录失败', + error: error.message + }); + } +}; + +/** + * 获取我的确认列表 + * GET /api/training-units/my-confirmations + * 需要登录 + */ +exports.getMyConfirmations = async (req, res) => { + try { + const userId = req.user.id; + + const [confirmations] = await db.query(` + SELECT + tu.*, + tc.confirmed_at + FROM training_confirmations tc + JOIN training_units tu ON tc.training_unit_id = tu.id + WHERE tc.user_id = ? + ORDER BY tc.confirmed_at DESC + `, [userId]); + + const formattedData = confirmations.map(c => ({ + id: c.id, + title: c.title, + start_date: c.start_date, + related_positions: c.related_positions, + related_companies: c.related_companies, + confirmed_at: c.confirmed_at + })); + + return res.json({ + success: true, + data: formattedData + }); + } catch (error) { + console.error('获取确认列表失败:', error); + return res.status(500).json({ + success: false, + message: '获取确认列表失败', + error: error.message + }); + } +}; diff --git a/api/controllers/transitionProgressController.js b/api/controllers/transitionProgressController.js new file mode 100644 index 0000000..aa5496e --- /dev/null +++ b/api/controllers/transitionProgressController.js @@ -0,0 +1,270 @@ +/** + * 面试进度控制器 + * 基于high.html的最新数据结构 + */ + +const db = require('../../config/database'); + +/** + * 获取我的面试进度列表 + * GET /api/transition-progress + * 需要登录 + */ +exports.getMyProgress = async (req, res) => { + try { + const userId = req.user.id; + + const [records] = await db.query( + `SELECT + id, + company, + job, + status, + status_type, + created_at, + updated_at + FROM transition_progress + WHERE user_id = ? + ORDER BY created_at DESC`, + [userId] + ); + + return res.json({ + success: true, + data: records + }); + } catch (error) { + console.error('获取面试进度失败:', error); + return res.status(500).json({ + success: false, + message: '获取面试进度失败', + error: error.message + }); + } +}; + +/** + * 创建面试进度记录 + * POST /api/transition-progress + * 需要登录 + */ +exports.create = async (req, res) => { + try { + const userId = req.user.id; + const { + company, + job, + status, + status_type + } = req.body; + + // 验证必填字段 + if (!company || !job || !status) { + return res.status(400).json({ + success: false, + message: '缺少必填字段 (company, job, status)' + }); + } + + // 验证status_type + if (status_type && !['offer', 'hr', 'ing', 'pending'].includes(status_type)) { + return res.status(400).json({ + success: false, + message: 'status_type 必须是 offer, hr, ing 或 pending' + }); + } + + const [result] = await db.query( + `INSERT INTO transition_progress + (user_id, company, job, status, status_type) + VALUES (?, ?, ?, ?, ?)`, + [ + userId, + company, + job, + status, + status_type || 'pending' + ] + ); + + return res.status(201).json({ + success: true, + message: '面试进度记录创建成功', + data: { id: result.insertId } + }); + } catch (error) { + console.error('创建面试进度失败:', error); + return res.status(500).json({ + success: false, + message: '创建面试进度失败', + error: error.message + }); + } +}; + +/** + * 更新面试进度 + * PUT /api/transition-progress/:id + * 需要登录 + */ +exports.update = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.id; + const updateData = req.body; + + // 检查记录是否存在且属于当前用户 + const [existing] = await db.query( + 'SELECT id FROM transition_progress WHERE id = ? AND user_id = ?', + [id, userId] + ); + + if (existing.length === 0) { + return res.status(404).json({ + success: false, + message: '面试进度记录不存在或无权访问' + }); + } + + const updates = []; + const params = []; + + const allowedFields = ['company', 'job', 'status', 'status_type']; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + // 验证status_type + if (field === 'status_type' && !['offer', 'hr', 'ing', 'pending'].includes(updateData[field])) { + return res.status(400).json({ + success: false, + message: 'status_type 必须是 offer, hr, ing 或 pending' + }); + } + updates.push(`${field} = ?`); + params.push(updateData[field]); + } + } + + if (updates.length === 0) { + return res.status(400).json({ + success: false, + message: '没有要更新的字段' + }); + } + + params.push(id); + await db.query( + `UPDATE transition_progress SET ${updates.join(', ')} WHERE id = ?`, + params + ); + + return res.json({ + success: true, + message: '面试进度更新成功' + }); + } catch (error) { + console.error('更新面试进度失败:', error); + return res.status(500).json({ + success: false, + message: '更新面试进度失败', + error: error.message + }); + } +}; + +/** + * 删除面试进度记录 + * DELETE /api/transition-progress/:id + * 需要登录 + */ +exports.delete = async (req, res) => { + try { + const { id } = req.params; + const userId = req.user.id; + + const [result] = await db.query( + 'DELETE FROM transition_progress WHERE id = ? AND user_id = ?', + [id, userId] + ); + + if (result.affectedRows === 0) { + return res.status(404).json({ + success: false, + message: '面试进度记录不存在或无权访问' + }); + } + + return res.json({ + success: true, + message: '面试进度删除成功' + }); + } catch (error) { + console.error('删除面试进度失败:', error); + return res.status(500).json({ + success: false, + message: '删除面试进度失败', + error: error.message + }); + } +}; + +/** + * 获取所有面试进度记录 (管理员) + * GET /api/transition-progress/all + * 需要管理员权限 + */ +exports.getAll = async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 20; + const offset = (page - 1) * limit; + const statusType = req.query.status_type; + + // 构建查询条件 + let whereClause = ''; + const params = []; + + if (statusType && ['offer', 'hr', 'ing', 'pending'].includes(statusType)) { + whereClause = 'WHERE status_type = ?'; + params.push(statusType); + } + + // 查询总数 + const [countResult] = await db.query( + `SELECT COUNT(*) as total FROM transition_progress ${whereClause}`, + params + ); + const total = countResult[0].total; + + // 查询记录列表 + const [records] = await db.query( + `SELECT + tp.*, + u.username + FROM transition_progress tp + LEFT JOIN users u ON tp.user_id = u.id + ${whereClause} + ORDER BY tp.created_at DESC + LIMIT ? OFFSET ?`, + [...params, limit, offset] + ); + + return res.json({ + success: true, + data: { + records, + total, + page, + limit, + totalPages: Math.ceil(total / limit) + } + }); + } catch (error) { + console.error('获取所有面试进度失败:', error); + return res.status(500).json({ + success: false, + message: '获取所有面试进度失败', + error: error.message + }); + } +}; diff --git a/api/routes/applications.js b/api/routes/applications.js new file mode 100644 index 0000000..60d584e --- /dev/null +++ b/api/routes/applications.js @@ -0,0 +1,20 @@ +/** + * 投递记录路由 + */ + +const express = require('express'); +const router = express.Router(); +const applicationController = require('../controllers/applicationController'); +const { verifyToken } = require('../../middleware/auth'); + +// 所有路由都需要认证 +router.use(verifyToken); + +// 投递记录相关路由 +router.post('/', applicationController.createApplication); +router.get('/', applicationController.getApplications); +router.get('/statistics', applicationController.getStatistics); +router.get('/:id', applicationController.getApplicationDetail); +router.put('/:id/withdraw', applicationController.withdrawApplication); + +module.exports = router; diff --git a/api/routes/auth.js b/api/routes/auth.js new file mode 100644 index 0000000..2fa84ad --- /dev/null +++ b/api/routes/auth.js @@ -0,0 +1,18 @@ +/** + * 用户认证路由 + */ + +const express = require('express'); +const router = express.Router(); +const authController = require('../controllers/authController'); +const { verifyToken } = require('../../middleware/auth'); + +// 公开路由(无需认证) +router.post('/register', authController.register); +router.post('/login', authController.login); + +// 需要认证的路由 +router.get('/me', verifyToken, authController.getCurrentUser); +router.put('/profile', verifyToken, authController.updateProfile); + +module.exports = router; diff --git a/api/routes/highSalaryJobs.js b/api/routes/highSalaryJobs.js new file mode 100644 index 0000000..b3190a3 --- /dev/null +++ b/api/routes/highSalaryJobs.js @@ -0,0 +1,19 @@ +/** + * 高薪岗位路由 + */ + +const express = require('express'); +const router = express.Router(); +const controller = require('../controllers/highSalaryJobController'); +const { optionalAuth, verifyToken, verifyAdmin } = require('../../middleware/auth'); + +// 公开/可选登录路由 +router.get('/', optionalAuth, controller.getList); +router.get('/:id', optionalAuth, controller.getById); + +// 管理员路由 +router.post('/', verifyToken, verifyAdmin, controller.create); +router.put('/:id', verifyToken, verifyAdmin, controller.update); +router.delete('/:id', verifyToken, verifyAdmin, controller.delete); + +module.exports = router; diff --git a/api/routes/successStories.js b/api/routes/successStories.js new file mode 100644 index 0000000..d47f80e --- /dev/null +++ b/api/routes/successStories.js @@ -0,0 +1,18 @@ +/** + * 成功案例路由 + */ + +const express = require('express'); +const router = express.Router(); +const controller = require('../controllers/successStoryController'); +const { verifyToken, verifyAdmin } = require('../../middleware/auth'); + +// 公开路由(无需登录) +router.get('/', controller.getList); + +// 管理员路由 +router.post('/', verifyToken, verifyAdmin, controller.create); +router.put('/:id', verifyToken, verifyAdmin, controller.update); +router.delete('/:id', verifyToken, verifyAdmin, controller.delete); + +module.exports = router; diff --git a/api/routes/trainingUnits.js b/api/routes/trainingUnits.js new file mode 100644 index 0000000..92efe33 --- /dev/null +++ b/api/routes/trainingUnits.js @@ -0,0 +1,18 @@ +/** + * 培训单元路由 + */ + +const express = require('express'); +const router = express.Router(); +const controller = require('../controllers/trainingUnitController'); +const { optionalAuth, verifyToken } = require('../../middleware/auth'); + +// 可选登录路由(未登录也可访问,但登录后会显示确认状态) +router.get('/', optionalAuth, controller.getList); + +// 需要登录的路由 +router.post('/:id/confirm', verifyToken, controller.confirm); +router.delete('/:id/confirm', verifyToken, controller.cancelConfirm); +router.get('/my-confirmations', verifyToken, controller.getMyConfirmations); + +module.exports = router; diff --git a/api/routes/transitionProgress.js b/api/routes/transitionProgress.js new file mode 100644 index 0000000..ee6bad5 --- /dev/null +++ b/api/routes/transitionProgress.js @@ -0,0 +1,19 @@ +/** + * 面试进度路由 + */ + +const express = require('express'); +const router = express.Router(); +const controller = require('../controllers/transitionProgressController'); +const { verifyToken, verifyAdmin } = require('../../middleware/auth'); + +// 需要登录的路由 +router.get('/', verifyToken, controller.getMyProgress); +router.post('/', verifyToken, controller.create); +router.put('/:id', verifyToken, controller.update); +router.delete('/:id', verifyToken, controller.delete); + +// 管理员路由 +router.get('/all', verifyToken, verifyAdmin, controller.getAll); + +module.exports = router; diff --git a/auth.html b/auth.html new file mode 100644 index 0000000..e07d9c2 --- /dev/null +++ b/auth.html @@ -0,0 +1,387 @@ + + + + + + 大专生就业服务平台 - 登录/注册 + + + + +
+ +
+ +
+

欢迎回来

+

登录就业服务平台

+
+
+ +
+
+ +
+
+ +
+
+ 还没有账号?去注册 +
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/config/database.js b/config/database.js new file mode 100644 index 0000000..7d0233c --- /dev/null +++ b/config/database.js @@ -0,0 +1,32 @@ +/** + * 数据库连接配置 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +// 数据库连接池配置 +const pool = mysql.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0, + enableKeepAlive: true, + keepAliveInitialDelay: 0 +}); + +// 测试数据库连接 +pool.getConnection() + .then(connection => { + console.log('✅ 数据库连接成功'); + connection.release(); + }) + .catch(err => { + console.error('❌ 数据库连接失败:', err.message); + }); + +module.exports = pool; diff --git a/css/components.css b/css/components.css index 3d7ef34..b654ab9 100644 --- a/css/components.css +++ b/css/components.css @@ -4,8 +4,9 @@ /* V1 风格霸气标题 */ .v1-title { - font-family: 'Arial Black', 'Helvetica Neue', sans-serif; + font-family: 'Source Han Sans CN Heavy', 'Source Han Sans SC Heavy', 'PingFang SC Heavy', 'Heiti SC', 'SimHei', 'Microsoft YaHei Bold', 'Arial Black', 'Helvetica Neue', sans-serif; font-size: 5rem; + font-weight: 900; letter-spacing: 0.8rem; color: #fff; text-shadow: 0 0 20px rgba(0, 240, 255, 0.8), 0 0 40px rgba(0, 240, 255, 0.4); @@ -30,42 +31,42 @@ bottom: 10%; left: 50%; transform: translateX(-50%); - color: rgba(0, 240, 255, 0.8); + color: rgba(56, 189, 248, 0.9); font-size: 0.9rem; pointer-events: none; z-index: 10; letter-spacing: 2px; - border: 1px solid rgba(0, 240, 255, 0.3); + border: 1px solid rgba(56, 189, 248, 0.4); padding: 10px 20px; border-radius: 30px; - background: rgba(0, 0, 0, 0.5); + background: rgba(255, 255, 255, 0.8); opacity: 0; /* 初始隐藏,等待开场动画 */ } /* 毛玻璃头部 */ .glass-header { - background: rgba(11, 16, 38, 0.9); + background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(12px); - border-bottom: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid rgba(147, 197, 253, 0.3); } /* 搜索框 */ .search-input { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.1); - color: white; + background: rgba(100, 116, 139, 0.08); + border: 1px solid rgba(100, 116, 139, 0.2); + color: #1e293b; transition: all 0.3s; } .search-input:focus { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.9); border-color: #38bdf8; outline: none; box-shadow: 0 0 15px rgba(56, 189, 248, 0.2); } .search-input::placeholder { - color: rgba(255, 255, 255, 0.4); + color: rgba(30, 41, 59, 0.5); } /* 搜索建议下拉列表 */ @@ -74,13 +75,13 @@ top: calc(100% + 8px); left: 0; right: 0; - background: rgba(17, 24, 39, 0.95); + background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.1); + border: 1px solid rgba(147, 197, 253, 0.3); max-height: 320px; overflow-y: auto; - box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + box-shadow: 0 10px 40px rgba(100, 116, 139, 0.2); z-index: 10000; } @@ -91,9 +92,9 @@ display: flex; align-items: center; gap: 10px; - color: #e5e7eb; + color: #475569; transition: all 0.2s; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); + border-bottom: 1px solid rgba(147, 197, 253, 0.2); } .search-item:last-child { @@ -102,8 +103,8 @@ .search-item:hover, .search-item.active { - background: rgba(59, 130, 246, 0.2); - color: #60a5fa; + background: rgba(56, 189, 248, 0.1); + color: #0369a1; } /* 搜索项图标 */ @@ -167,7 +168,7 @@ /* 企业卡片 */ .company-card { - background: rgba(30, 41, 59, 0.6); + background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(56, 189, 248, 0.2); backdrop-filter: blur(10px); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); @@ -178,13 +179,13 @@ .company-card:hover { transform: translateY(-5px); border-color: #38bdf8; - background: rgba(30, 41, 59, 0.8); - box-shadow: 0 0 30px rgba(56, 189, 248, 0.15); + background: rgba(255, 255, 255, 0.9); + box-shadow: 0 0 30px rgba(56, 189, 248, 0.2); } /* 职位卡片(通用) */ .job-card { - background: rgba(30, 41, 59, 0.6); + background: rgba(255, 255, 255, 0.7); border: 1px solid rgba(56, 189, 248, 0.2); backdrop-filter: blur(10px); transition: all 0.3s; @@ -193,20 +194,20 @@ .job-card:hover { transform: translateY(-5px); border-color: #38bdf8; - background: rgba(30, 41, 59, 0.8); + background: rgba(255, 255, 255, 0.9); box-shadow: 0 10px 30px -10px rgba(56, 189, 248, 0.3); } /* 详情页 - 业务板块卡片 */ .segment-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.05); + background: rgba(239, 246, 255, 0.5); + border: 1px solid rgba(147, 197, 253, 0.3); border-radius: 8px; padding: 20px; } .segment-title { - color: #fff; + color: #0f172a; font-weight: bold; font-size: 1.1rem; margin-bottom: 15px; diff --git a/css/layout.css b/css/layout.css index 6cbb5dd..1d20ea3 100644 --- a/css/layout.css +++ b/css/layout.css @@ -68,14 +68,14 @@ height: 100%; z-index: 5; display: none; - background: radial-gradient(circle at 50% 30%, #1e293b 0%, #0b1026 100%); + background: radial-gradient(circle at 50% 30%, #e0f2fe 0%, #f8fafc 100%); } #list-interface { z-index: 6; - background: #0b1026; - background-image: linear-gradient(rgba(56, 189, 248, 0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(56, 189, 248, 0.03) 1px, transparent 1px); + background: #f8fafc; + background-image: linear-gradient(rgba(56, 189, 248, 0.08) 1px, transparent 1px), + linear-gradient(90deg, rgba(56, 189, 248, 0.08) 1px, transparent 1px); background-size: 40px 40px; } @@ -89,7 +89,7 @@ z-index: 7; display: none; opacity: 0; - background: #050b14; + background: #f8fafc; } /* 面包屑导航 */ diff --git a/css/website.css b/css/website.css index 94717e5..c83f8af 100644 --- a/css/website.css +++ b/css/website.css @@ -183,14 +183,14 @@ .dot { width: 8px; height: 8px; - background: rgba(255, 255, 255, 0.3); + background: #cbd5e1; /* 浅灰色,适配亮色主题 */ border-radius: 50%; transition: all 0.3s ease; cursor: pointer; } .dot.active { - background: #38bdf8; + background: #2563eb; /* 蓝色,适配亮色主题 */ width: 24px; border-radius: 4px; } diff --git a/high copy.html b/high copy.html new file mode 100644 index 0000000..fa404cb --- /dev/null +++ b/high copy.html @@ -0,0 +1,1719 @@ + + + + + + 高薪岗位直通车 - 旗舰指挥舱 + + + + + + + + + +
+ +
+
+ +
我的状态
+
+ +
+
+
+
+

必要资质要求

+

过渡岗位工作经历证明

+
+
+ 已获得 +
+
+ +
+
+ +
+
+ +
智能制造班
+
+
+
+ +
+
+ 我的高薪岗位面试 + +
+ +
+
+
字节跳动
+
后端开发工程师
+
Offer发放
+
+
+
蔚来汽车
+
自动驾驶算法助理
+
HR复试
+
+
+
哔哩哔哩
+
前端开发 (React)
+
二面考核
+
+
+
京东商城
+
物流数据分析
+
初试邀请
+
+
+
米哈游
+
游戏客户端开发
+
笔试通过
+
+
+
腾讯游戏
+
关卡策划
+
简历评估
+
+
+
+
+
+ +
+
+ +
高薪岗位预招录确认
+
+ +
+
+
+
SOLIDWORKS与钣金设计类岗位
+
+ 2026/4/20 开课 +
+ +
+
+
+
对应岗位
+
+
焊接工艺工程师
焊接技术员
钣金工程师
+
+
+
+
相关企业
+
+
吉利控股集团有限公司
保定成聚模具冲压有限公司
佳创机械设备制造(固安)有限公司
浙江华岳包装机械有限公司
江阴兴澄特种钢铁有限公司
江苏交通控股有限公司
徐州工程机械集团有限公司
+
+
+
+
+ +
+
+
UG编程与CNC编程类岗位
+
+ 2026/4/25 开课 +
+ +
+
+
+
对应岗位
+
+
CNC编程工程师
刀具应用工程师
刀具技术工程师
CNC技术员
CNC调试员
+
+
+
+
相关企业
+
+
盛虹控股集团有限公司
江苏沙钢集团有限公司
协鑫集团有限公司
中天钢铁集团有限公司
江苏新长江实业集团有限公司
苏美达股份有限公司
+
+
+
+
+ +
+
+
模具设计类岗位必备核心技能
+
+ 2026/5/5 开课 +
+ +
+
+
+
对应岗位
+
+
模具设计师
模具质检员
模具质量工程师
模具工
模具工程师
+
+
+
+
相关企业
+
+
江苏江中集团有限公司
无锡江南电缆有限公司
江苏长电科技股份有限公司
江苏扬子江船业集团有限公司
中车唐山机车车辆有限公司
+
+
+
+
+ +
+
+
快速上手PLC编程调试类岗位
+
+ 2026/5/11 开课 +
+ +
+
+
+
对应岗位
+
+
PLC编程工程师
电气控制工程师
PLC自动化工程师
PLC工程师
电气自动化管培生
+
+
+
+
相关企业
+
+
上海汽车集团股份有限公司
中国石化上海石油化工股份有限公司
上海华谊控股集团有限公司
上海通用电焊机股份有限公司
+
+
+
+
+ +
+
+
工业机器人运维调试类岗位实战技巧
+
+ 2026/5/18 开课 +
+ +
+
+
+
对应岗位
+
+
工业机器人工程师
工业机器人调试工程师
工业机器人调试技术员
工业机器人工程师助理
+
+
+
+
相关企业
+
+
奇瑞控股集团有限公司
华勤技术股份有限公司
蔚来集团
美的集团股份有限公司
TCL科技集团股份有限公司
立讯精密工业股份有限公司
+
+
+
+
+ +
+
+
机器识别类岗位上岗必学技能
+
+ 2026/5/25 开课 +
+ +
+
+
+
对应岗位
+
+
视觉检测工程师
机器视觉工程师
机器视觉应用工程师
机器视觉调试技术员
机器视觉调试工程师
+
+
+
+
相关企业
+
+
中天科技集团有限公司
立铠精密科技(盐城)有限公司
远景能源有限公司
天合光能股份有限公司
惠州亿纬锂能股份有限公司
广东伊之密精密机械股份有限公司
+
+
+
+
+ +
+
+
C#与上位机;高阶岗位技能
+
+ 2026/6/1 开课 +
+ +
+
+
+
对应岗位
+
+
工业机器人工程师
PLC自动化工程师
自动化工程师
通信协议工程师
机器视觉应用工程师
+
+
+
+
相关企业
+
+
恒力集团有限公司
亨通集团有限公司
永鼎集团有限公司
江苏沃得机电集团有限公司
重庆长线智能科技有限责任公司
宁波均胜电子股份有限公司
+
+
+
+
+ +
+
+
非标自动化实战与行业发展精进
+
+ 2026/6/8 开课 +
+ +
+
+
+
对应岗位
+
+
非标自动化工艺工程师
非标自动化结构工程师
非标设备电气工程师
非标自动化工程师
+
+
+
+
相关企业
+
+
宿迁阿特斯阳光能源科技有限公司
重庆钢铁股份有限公司
重庆宗申发动机制造有限公司
重庆美利信科技股份有限公司
重庆京东方光电科技有限公司
江苏恒瑞医药股份有限公司
+
+
+
+
+
+
+ +
+
+ +
恭喜以下学员成功入职高薪岗位
+
+ +
+
+
赵**自动化技术员@恒力集团有限公司9k
+
钱**工业机器人调试工程师@亨通集团有限公司11k
+
孙**非标自动化工程师@盛虹控股集团有限公司10k
+
李**自动化设备调试工程师@江苏沙钢集团有限公司12k
+
周**自动化仪表工程师@协鑫集团有限公司8k
+
吴**电气自动化工程师@新誉集团有限公司9k
+
郑**PLC工程师@中天科技集团有限公司11k
+
王**自动化产线维护员@海澜集团有限公司8k
+
冯**机电工程师@红豆集团有限公司10k
+
陈**自动化控制技术员@江苏国泰国际集团股份有限公司12k
+
褚**智能制造工程师@瑞声科技控股有限公司9k
+
卫**机器人维护工程师@天合光能股份有限公司11k
+ +
赵**自动化技术员@恒力集团有限公司9k
+
钱**工业机器人调试工程师@亨通集团有限公司11k
+
孙**非标自动化工程师@盛虹控股集团有限公司10k
+
李**自动化设备调试工程师@江苏沙钢集团有限公司12k
+
周**自动化仪表工程师@协鑫集团有限公司8k
+
吴**电气自动化工程师@新誉集团有限公司9k
+
郑**PLC工程师@中天科技集团有限公司11k
+
王**自动化产线维护员@海澜集团有限公司8k
+
冯**机电工程师@红豆集团有限公司10k
+
陈**自动化控制技术员@江苏国泰国际集团股份有限公司12k
+
褚**智能制造工程师@瑞声科技控股有限公司9k
+
卫**机器人维护工程师@天合光能股份有限公司11k
+
+ +
+
蒋**自动化测试工程师@信达生物制药(苏州)有限公司12k
+
沈**电气设计师@同程网络科技股份有限公司10k
+
韩**设备自动化工程师@江苏新潮科技集团有限公司9k
+
杨**工控网络工程师@远东控股集团有限公司11k
+
朱**自动化系统集成工程师@江苏中南建设集团股份有限公司12k
+
秦**现场应用工程师@苏宁易购集团股份有限公司8k
+
尤**自动化运维工程师@南京钢铁集团有限公司9k
+
许**视觉算法工程师(助理)@江苏扬子江船厂有限公司12k
+
何**运动控制算法工程师@徐工集团工程机械有限公司11k
+
吕**自动化项目经理助理@中信泰富特钢集团股份有限公司10k
+
施**电气调试员@江苏悦达集团有限公司8k
+
张**自动化装配技术员@东方润安集团有限公司9k
+ +
蒋**自动化测试工程师@信达生物制药(苏州)有限公司12k
+
沈**电气设计师@同程网络科技股份有限公司10k
+
韩**设备自动化工程师@江苏新潮科技集团有限公司9k
+
杨**工控网络工程师@远东控股集团有限公司11k
+
朱**自动化系统集成工程师@江苏中南建设集团股份有限公司12k
+
秦**现场应用工程师@苏宁易购集团股份有限公司8k
+
尤**自动化运维工程师@南京钢铁集团有限公司9k
+
许**视觉算法工程师(助理)@江苏扬子江船厂有限公司12k
+
何**运动控制算法工程师@徐工集团工程机械有限公司11k
+
吕**自动化项目经理助理@中信泰富特钢集团股份有限公司10k
+
施**电气调试员@江苏悦达集团有限公司8k
+
张**自动化装配技术员@东方润安集团有限公司9k
+
+ +
+
孔**自动化售后工程师@江苏永钢集团有限公司8k
+
曹**MES实施工程师@江苏恒顺集团有限公司9k
+
严**SCADA开发工程师@江苏索普(集团)有限公司11k
+
华**嵌入式软件工程师@通鼎集团有限公司12k
+
金**自动化硬件工程师@波司登股份有限公司10k
+
魏**工控机维护员@扬州泰富特种材料有限公司8k
+
陶**自动化仓储运维@江苏飞达控股集团有限公司9k
+
姜**AGV调度工程师@大全集团有限公司11k
+
戚**自动化采购专员@弘元绿色能源股份有限公司8k
+
谢**技术支持工程师@亚邦投资控股集团有限公司10k
+
邹**自动化培训讲师@江苏林洋能源股份有限公司12k
+ +
孔**自动化售后工程师@江苏永钢集团有限公司8k
+
曹**MES实施工程师@江苏恒顺集团有限公司9k
+
严**SCADA开发工程师@江苏索普(集团)有限公司11k
+
华**嵌入式软件工程师@通鼎集团有限公司12k
+
金**自动化硬件工程师@波司登股份有限公司10k
+
魏**工控机维护员@扬州泰富特种材料有限公司8k
+
陶**自动化仓储运维@江苏飞达控股集团有限公司9k
+
姜**AGV调度工程师@大全集团有限公司11k
+
戚**自动化采购专员@弘元绿色能源股份有限公司8k
+
谢**技术支持工程师@亚邦投资控股集团有限公司10k
+
邹**自动化培训讲师@江苏林洋能源股份有限公司12k
+
+
+
+ +
+
+ +
当日高薪岗位信息
+
+ +
+
+
+
+
4K-6K
+
+
+
自动化技术员
+
+
+ 恒力集团有限公司 +
+
+ 苏州市 +
+
+ +
+
+ +
+
+
+
6-8K
+
+
+
自动化技术员
+
+
+ 东莞市宇瞳光学科技股份有限公司 +
+
+ 东莞市 +
+
+ +
+
+ +
+
+
+
8-10K
+
+
+
工业机器人调试工程师
+
+
+ 爱仕达股份有限公司 +
+
+ 温州 +
+
+ +
+
+ +
+
+
+
7-10k
+
+
+
非标自动化工程师
+
+
+ 光深自动化有限公司 +
+
+ 西安 +
+
+ +
+
+ +
+
+
+
7-9k
+
+
+
自动化设备调试工程师
+
+
+ 深圳市标王工业设备有限公司 +
+
+ 东莞 +
+
+ +
+
+ +
+
+
+
6-10K
+
+
+
自动化调试工程师
+
+
+ 苏州科林利华电子有限公司 +
+
+ 苏州 +
+
+ +
+
+ +
+
+
+
5-8K
+
+
+
机器人应用工程师
+
+
+ 佛山华数机器人有限公司 +
+
+ 佛山 +
+
+ +
+
+ +
+
+
+
10-15K
+
+
+
自动化工程师
+
+
+ 成都宏明双新科技股份有限公司 +
+
+ 成都 +
+
+ +
+
+ +
+
+
+
5-8K
+
+
+
自动化调试工程师
+
+
+ 湖南德沃智能制造有限公司 +
+
+ 长沙 +
+
+ +
+
+ +
+
+
+
7-9K
+
+
+
电气自动化工程师
+
+
+ 江西联创电子有限公司 +
+
+ 南昌 +
+
+ +
+
+ +
+
+
+
8-12K
+
+
+
设备调试工程师
+
+
+ 宁德时代新能源科技股份有限公司 +
+
+ 宁德 +
+
+ +
+
+ +
+
+
+
5-7K
+
+
+
自动化助理工程师
+
+
+ 郑州富士康科技集团 +
+
+ 郑州 +
+
+ +
+
+ +
+
+
+
6-9K
+
+
+
机器人维护工程师
+
+
+ 杭州海康威视数字技术股份有限公司 +
+
+ 杭州 +
+
+ +
+
+ +
+
+
+
7-10K
+
+
+
PLC自动化工程师
+
+
+ 厦门盈趣科技股份有限公司 +
+
+ 厦门 +
+
+ +
+
+ +
+
+
+
6-8K
+
+
+
产线调试技术员
+
+
+ 重庆长安汽车股份有限公司 +
+
+ 重庆 +
+
+ +
+
+ +
+
+
+
9-13K
+
+
+
自动化系统集成工程师
+
+
+ 青岛海尔集团 +
+
+ 青岛 +
+
+ +
+
+ +
+
+
+
8-12K
+
+
+
工业机器人现场工程师
+
+
+ 埃斯顿自动化股份有限公司 +
+
+ 南京 +
+
+ +
+
+ +
+
+
+
6-9K
+
+
+
电气控制技术员
+
+
+ 天津长城汽车哈弗分公司 +
+
+ 天津 +
+
+ +
+
+ +
+
+
+
7-10K
+
+
+
设备维护调试员
+
+
+ 合肥京东方光电科技有限公司 +
+
+ 合肥 +
+
+ +
+
+ +
+
+
+
5-8K
+
+
+
初级自动化技术员
+
+
+ 昆山龙腾光电股份有限公司 +
+
+ 昆山 +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/high.html b/high.html new file mode 100644 index 0000000..3e5f44a --- /dev/null +++ b/high.html @@ -0,0 +1,1080 @@ + + + + + + 高薪岗位直通车 - 旗舰指挥舱 + + + + + + + + + +
+ +
+
+ +
我的状态
+
+ +
+
+
+
+

必要资质要求

+

过渡岗位工作经历证明

+
+
+ 已获得 +
+
+ +
+
+ +
+
+ +
智能制造班
+
+
+
+ +
+
+ 我的高薪岗位面试 + +
+ +
+
+
字节跳动
+
后端开发工程师
+
Offer发放
+
+
+
蔚来汽车
+
自动驾驶算法助理
+
HR复试
+
+
+
哔哩哔哩
+
前端开发 (React)
+
二面考核
+
+
+
京东商城
+
物流数据分析
+
初试邀请
+
+
+
米哈游
+
游戏客户端开发
+
笔试通过
+
+
+
腾讯游戏
+
关卡策划
+
简历评估
+
+
+
+
+
+ +
+
+ +
高薪岗位预招录确认
+
+ +
+
+
+ +
+
+ +
恭喜以下学员成功入职高薪岗位
+
+ +
+
+
+
+
+
+ +
+
+ +
当日高薪岗位信息
+
+ +
+
+
+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/index copy.html b/index copy.html new file mode 100644 index 0000000..7d0707d --- /dev/null +++ b/index copy.html @@ -0,0 +1,1600 @@ + + + + + + + + + 大专生就业服务平台 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/index.html b/index.html index a615f6a..a23de88 100644 --- a/index.html +++ b/index.html @@ -8,17 +8,22 @@ 大专生就业服务平台 - - - + + + - -
-
+ - -
-
-
-

多多畅职大专生就业服务平台

-

CAREER SERVICE PLATFORM

-
-
- -
-
-
-
-
-
-

企业资源

- 优质岗位 · 名企直推 -
- - - - - - - - - - - -
- - - -
-
- -

苏州市 · 企业名录

-
-
- -
- +
+
+
+
+
+ + + +
- - - - - - -
- - - - - - - - + + + + + + // 3D内推平台点击逻辑 + document.getElementById('referral-split-card').addEventListener('click', function(e) { + e.preventDefault(); e.stopPropagation(); + document.getElementById('home-wrapper').classList.add('hidden'); + document.getElementById('app-container').classList.remove('hidden'); + const tryStartApp = () => { + if (window.appInstance && typeof window.appInstance.switchToApp === 'function') { + window.appInstance.switchToApp(); + } else { setTimeout(tryStartApp, 50); } + }; + tryStartApp(); + }); + + // 教务系统点击 + document.getElementById('education-card').addEventListener('click', function(e) { + // 移动端点击被遮罩层拦截,此处不需要额外处理,除非需要JS逻辑 + if (window.innerWidth > 768) { + e.preventDefault(); + document.getElementById('home-wrapper').classList.add('hidden'); + document.getElementById('education-iframe').classList.remove('hidden'); - - + + + + + + - - - - - - + }); + + + + + diff --git a/js/auth.js b/js/auth.js new file mode 100644 index 0000000..99212df --- /dev/null +++ b/js/auth.js @@ -0,0 +1,350 @@ +/** + * 用户认证逻辑 + * 处理登录、注册表单的提交和验证 + */ + +// API配置 +const API_BASE_URL = 'http://192.168.1.17:8080/api'; +const AUTH_TOKEN_KEY = 'duoduo_auth_token'; +const USER_INFO_KEY = 'duoduo_user_info'; + +// 专业大类数据 +let professionalCategories = []; + +/** + * 加载专业大类数据 + */ +async function loadProfessionalCategories() { + try { + const response = await fetch('/专业大类.json'); + professionalCategories = await response.json(); + + // 填充注册表单的下拉选择框 + const selectElement = document.getElementById('register-professional-category'); + if (selectElement) { + professionalCategories.forEach(category => { + const option = document.createElement('option'); + option.value = category.大类代码; + option.textContent = category.大类名称; + selectElement.appendChild(option); + }); + } + } catch (error) { + console.error('加载专业大类失败:', error); + } +} + +// ==================== 工具函数 ==================== + +/** + * 显示错误信息 + */ +function showError(elementId, message) { + const errorElement = document.getElementById(elementId); + errorElement.textContent = message; + errorElement.style.display = 'block'; +} + +/** + * 显示成功信息 + */ +function showSuccess(elementId, message) { + const successElement = document.getElementById(elementId); + successElement.textContent = message; + successElement.style.display = 'block'; +} + +/** + * 隐藏消息 + */ +function hideMessage(elementId) { + const element = document.getElementById(elementId); + element.textContent = ''; + element.style.display = 'none'; +} + +/** + * 隐藏所有消息 + */ +function hideAllMessages() { + hideMessage('login-error-msg'); + hideMessage('register-error-msg'); + hideMessage('register-success-msg'); +} + +// ==================== 表单验证 ==================== + +/** + * 验证登录表单 + */ +function validateLoginForm(username, password) { + if (!username || !password) { + return '用户名和密码不能为空'; + } + return null; +} + +/** + * 验证注册表单 + */ +function validateRegisterForm(username, password, confirmPassword, email, phone) { + // 真实姓名验证 + if (!username) { + return '真实姓名不能为空'; + } + if (username.length < 2 || username.length > 10) { + return '姓名长度必须在2-10个字符之间'; + } + + // 密码验证 + if (!password) { + return '密码不能为空'; + } + if (password.length < 6) { + return '密码长度至少为6个字符'; + } + + // 确认密码验证 + if (password !== confirmPassword) { + return '两次输入的密码不一致'; + } + + // 邮箱验证(如果填写) + if (email && email.trim() !== '') { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return '邮箱格式不正确'; + } + } + + // 手机号验证(必填) + if (!phone || phone.trim() === '') { + return '手机号码不能为空'; + } + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + return '手机号格式不正确'; + } + + return null; +} + +// ==================== 登录功能 ==================== + +/** + * 处理用户登录 + */ +async function handleLogin() { + // 隐藏之前的错误信息 + hideAllMessages(); + + // 获取表单数据 + const username = document.getElementById('login-username').value.trim(); + const password = document.getElementById('login-password').value; + + // 前端验证 + const validationError = validateLoginForm(username, password); + if (validationError) { + showError('login-error-msg', validationError); + return; + } + + // 禁用提交按钮,显示加载状态 + const submitBtn = document.getElementById('login-submit-btn'); + const originalText = submitBtn.textContent; + submitBtn.disabled = true; + submitBtn.textContent = '登录中...'; + submitBtn.style.opacity = '0.7'; + + try { + // 调用登录API + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + + if (data.success) { + // 保存token和用户信息到localStorage + localStorage.setItem(AUTH_TOKEN_KEY, data.data.token); + localStorage.setItem(USER_INFO_KEY, JSON.stringify(data.data.user)); + + // 登录成功,跳转到首页 + window.location.href = 'index.html'; + } else { + // 显示后端返回的错误信息 + showError('login-error-msg', data.message || '登录失败,请重试'); + } + } catch (error) { + console.error('登录错误:', error); + showError('login-error-msg', '网络错误,请检查服务器是否运行'); + } finally { + // 恢复按钮状态 + submitBtn.disabled = false; + submitBtn.textContent = originalText; + submitBtn.style.opacity = '1'; + } +} + +// ==================== 注册功能 ==================== + +/** + * 处理用户注册 + */ +async function handleRegister() { + // 隐藏之前的消息 + hideAllMessages(); + + // 获取表单数据 + const username = document.getElementById('register-username').value.trim(); + const password = document.getElementById('register-password').value; + const confirmPassword = document.getElementById('register-password-confirm').value; + const email = document.getElementById('register-email').value.trim(); + const phone = document.getElementById('register-phone').value.trim(); + const professionalCategoryCode = document.getElementById('register-professional-category').value; + + // 前端验证 + const validationError = validateRegisterForm(username, password, confirmPassword, email, phone); + if (validationError) { + showError('register-error-msg', validationError); + return; + } + + // 禁用提交按钮,显示加载状态 + const submitBtn = document.getElementById('register-submit-btn'); + const originalText = submitBtn.textContent; + submitBtn.disabled = true; + submitBtn.textContent = '注册中...'; + submitBtn.style.opacity = '0.7'; + + try { + // 调用注册API + const requestBody = { username, password }; + if (email) requestBody.email = email; + if (phone) requestBody.phone = phone; + // 如果选择了专业大类,添加到请求体 + if (professionalCategoryCode) { + requestBody.professional_category_code = professionalCategoryCode; + const category = professionalCategories.find(c => c.大类代码 === professionalCategoryCode); + if (category) { + requestBody.professional_category = category.大类名称; + } + } + + const response = await fetch(`${API_BASE_URL}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + const data = await response.json(); + + if (data.success) { + // 显示成功提示 + showSuccess('register-success-msg', '注册成功!正在跳转到登录...'); + + // 2秒后切换到登录表单并预填充用户名 + setTimeout(() => { + // 切换到登录表单 + switchToLogin(); + + // 预填充用户名 + document.getElementById('login-username').value = username; + + // 清空注册表单 + document.getElementById('register-username').value = ''; + document.getElementById('register-password').value = ''; + document.getElementById('register-password-confirm').value = ''; + document.getElementById('register-email').value = ''; + document.getElementById('register-phone').value = ''; + + hideAllMessages(); + }, 2000); + } else { + // 显示后端返回的错误信息 + showError('register-error-msg', data.message || '注册失败,请重试'); + } + } catch (error) { + console.error('注册错误:', error); + showError('register-error-msg', '网络错误,请检查服务器是否运行'); + } finally { + // 恢复按钮状态 + submitBtn.disabled = false; + submitBtn.textContent = originalText; + submitBtn.style.opacity = '1'; + } +} + +// ==================== 表单切换功能 ==================== + +/** + * 切换到注册表单 + */ +function switchToRegister() { + hideAllMessages(); + + const loginContainer = document.getElementById('login-form-container'); + const registerContainer = document.getElementById('register-form-container'); + + // 隐藏登录 + loginContainer.style.opacity = '0'; + setTimeout(() => { + loginContainer.classList.add('hidden'); + // 显示注册 + registerContainer.classList.remove('hidden'); + registerContainer.classList.add('fade-in'); + registerContainer.style.opacity = '1'; + }, 300); +} + +/** + * 切换到登录表单 + */ +function switchToLogin() { + hideAllMessages(); + + const loginContainer = document.getElementById('login-form-container'); + const registerContainer = document.getElementById('register-form-container'); + + // 隐藏注册 + registerContainer.classList.remove('fade-in'); + registerContainer.style.opacity = '0'; + + setTimeout(() => { + registerContainer.classList.add('hidden'); + registerContainer.style.opacity = '1'; + + // 显示登录 + loginContainer.classList.remove('hidden'); + loginContainer.style.opacity = '1'; + loginContainer.classList.add('fade-in'); + }, 300); +} + +// ==================== 页面初始化 ==================== + +document.addEventListener('DOMContentLoaded', () => { + // 加载专业大类数据 + loadProfessionalCategories(); + + // 检查是否已登录 + const token = localStorage.getItem(AUTH_TOKEN_KEY); + if (token) { + // 已登录,直接跳转到首页 + window.location.href = 'index.html'; + return; + } + + // 绑定切换按钮事件 + document.getElementById('switch-to-register').addEventListener('click', switchToRegister); + document.getElementById('switch-to-login').addEventListener('click', switchToLogin); + + // 绑定表单提交事件(通过form的onsubmit已经绑定,这里只是确保) + // 表单已通过内联 onsubmit 绑定 +}); diff --git a/js/ui/ListInterface.js b/js/ui/ListInterface.js index 7c1f47f..c23a65b 100644 --- a/js/ui/ListInterface.js +++ b/js/ui/ListInterface.js @@ -91,15 +91,15 @@ export class ListInterface { card.innerHTML = `
${company.shortName} -
+
-
+

${company.shortName}

${tagsHtml}
-

${company.intro}

-
- 热招: ${jobsPreview}... - 详情 → +

${company.intro}

+
+ 热招: ${jobsPreview}... + 详情 →
`; diff --git a/js/ui/MapInterface.js b/js/ui/MapInterface.js index fc77d3a..67324c7 100644 --- a/js/ui/MapInterface.js +++ b/js/ui/MapInterface.js @@ -113,9 +113,9 @@ export class MapInterface { const breadcrumb = document.getElementById('breadcrumb-container'); if (breadcrumb) { breadcrumb.innerHTML = ` - 全国 - / - ${provinceInfo.name} + 全国 + / + ${provinceInfo.name} `; breadcrumb.classList.remove('hidden'); } @@ -192,18 +192,18 @@ export class MapInterface { return { name: name, itemStyle: { - // 有数据的区域:金黄色高亮 - areaColor: hasData ? 'rgba(251, 191, 36, 0.35)' : 'rgba(11, 16, 38, 0.8)', - borderColor: hasData ? '#fbbf24' : '#1e293b', - borderWidth: hasData ? 2 : 0.5 + // 有数据的区域:深蓝色高亮 + areaColor: hasData ? 'rgba(2, 132, 199, 0.3)' : '#f1f5f9', + borderColor: hasData ? '#0284c7' : '#cbd5e1', + borderWidth: hasData ? 2 : 1 }, emphasis: { itemStyle: { - // 鼠标悬停:更亮的金黄色 - areaColor: hasData ? 'rgba(251, 191, 36, 0.6)' : 'rgba(30, 41, 59, 0.9)' + // 鼠标悬停:浅蓝色 + areaColor: hasData ? '#bae6fd' : 'rgba(241, 245, 249, 0.9)' }, label: { - color: hasData ? '#fff' : '#94a3b8', + color: hasData ? '#0369a1' : '#64748b', fontWeight: hasData ? 'bold' : 'normal' } }, @@ -224,12 +224,12 @@ export class MapInterface { }, label: { show: true, - color: '#94a3b8', + color: '#64748b', // 改为深灰色,适配亮色背景 fontSize: isMobile ? 8 : 10 // 移动端使用更小字体 }, itemStyle: { - areaColor: '#0f172a', - borderColor: '#1e293b' + areaColor: '#f1f5f9', // 极浅的蓝灰色 + borderColor: '#cbd5e1' // 省份边界线 }, select: { disabled: true } }, diff --git a/js/utils/auth-helper.js b/js/utils/auth-helper.js new file mode 100644 index 0000000..0b716a1 --- /dev/null +++ b/js/utils/auth-helper.js @@ -0,0 +1,220 @@ +/** + * 认证工具模块 + * 用于其他页面检查登录状态、获取用户信息、退出登录等 + */ + +const AuthHelper = { + // 配置常量 + TOKEN_KEY: 'duoduo_auth_token', + USER_INFO_KEY: 'duoduo_user_info', + API_BASE_URL: 'http://192.168.1.17:8080/api', + + /** + * 获取存储的token + * @returns {string|null} token字符串或null + */ + getToken() { + return localStorage.getItem(this.TOKEN_KEY); + }, + + /** + * 获取存储的用户信息 + * @returns {Object|null} 用户信息对象或null + */ + getUserInfo() { + const userInfo = localStorage.getItem(this.USER_INFO_KEY); + return userInfo ? JSON.parse(userInfo) : null; + }, + + /** + * 检查是否已登录 + * @returns {boolean} 是否已登录 + */ + isLoggedIn() { + return !!this.getToken(); + }, + + /** + * 退出登录 + * 清除所有认证信息并跳转到登录页 + */ + logout() { + localStorage.removeItem(this.TOKEN_KEY); + localStorage.removeItem(this.USER_INFO_KEY); + window.location.href = 'auth.html'; + }, + + /** + * 验证token是否有效 + * 调用后端API验证token,如果无效则清除登录状态 + * @returns {Promise} token是否有效 + */ + async validateToken() { + const token = this.getToken(); + if (!token) { + return false; + } + + try { + const response = await fetch(`${this.API_BASE_URL}/auth/me`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + + if (data.success) { + // 更新用户信息 + localStorage.setItem(this.USER_INFO_KEY, JSON.stringify(data.data)); + return true; + } + } + + // Token无效,清除登录状态 + this.logout(); + return false; + } catch (error) { + console.error('Token验证失败:', error); + return false; + } + }, + + /** + * 路由守卫:保护需要登录的页面 + * 如果未登录则跳转到登录页 + * @param {string} redirectUrl 跳转的登录页URL,默认为auth.html + * @returns {boolean} 是否已登录 + */ + requireAuth(redirectUrl = 'auth.html') { + if (!this.isLoggedIn()) { + window.location.href = redirectUrl; + return false; + } + return true; + }, + + /** + * 获取带认证头的fetch配置 + * @param {Object} options fetch的配置对象 + * @returns {Object} 包含Authorization头的配置对象 + */ + getAuthHeaders(options = {}) { + const token = this.getToken(); + + return { + ...options, + headers: { + ...options.headers, + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }; + }, + + /** + * 发送认证请求的封装方法 + * @param {string} url 请求URL + * @param {Object} options fetch配置 + * @returns {Promise} 响应数据 + */ + async fetchWithAuth(url, options = {}) { + const response = await fetch(url, this.getAuthHeaders(options)); + + // 如果返回401未授权,清除登录状态并跳转 + if (response.status === 401) { + this.logout(); + return null; + } + + return response.json(); + }, + + /** + * 获取当前用户信息(从服务器获取最新) + * @returns {Promise} 用户信息或null + */ + async getCurrentUser() { + try { + const data = await this.fetchWithAuth(`${this.API_BASE_URL}/auth/me`); + + if (data && data.success) { + // 更新本地存储的用户信息 + localStorage.setItem(this.USER_INFO_KEY, JSON.stringify(data.data)); + return data.data; + } + + return null; + } catch (error) { + console.error('获取用户信息失败:', error); + return null; + } + }, + + /** + * 更新用户资料 + * @param {Object} profileData 要更新的资料数据 + * @returns {Promise} 是否更新成功 + */ + async updateProfile(profileData) { + try { + const data = await this.fetchWithAuth(`${this.API_BASE_URL}/auth/profile`, { + method: 'PUT', + body: JSON.stringify(profileData) + }); + + if (data && data.success) { + // 更新成功后刷新用户信息 + await this.getCurrentUser(); + return true; + } + + return false; + } catch (error) { + console.error('更新资料失败:', error); + return false; + } + }, + + /** + * 在页面中显示用户名 + * @param {string} elementId 要显示用户名的元素ID + */ + displayUsername(elementId) { + const userInfo = this.getUserInfo(); + if (userInfo && userInfo.username) { + const element = document.getElementById(elementId); + if (element) { + element.textContent = userInfo.username; + } + } + }, + + /** + * 在页面中显示用户完整信息 + * @param {Object} elementIds 元素ID映射对象 {username: 'username-el', email: 'email-el', ...} + */ + displayUserInfo(elementIds) { + const userInfo = this.getUserInfo(); + if (!userInfo) return; + + for (const [field, elementId] of Object.entries(elementIds)) { + const element = document.getElementById(elementId); + if (element && userInfo[field]) { + element.textContent = userInfo[field]; + } + } + } +}; + +// 如果在浏览器环境中,暴露到全局 +if (typeof window !== 'undefined') { + window.AuthHelper = AuthHelper; +} + +// 支持ES6模块导出 +if (typeof module !== 'undefined' && module.exports) { + module.exports = AuthHelper; +} diff --git a/js/website.js b/js/website.js index fee6810..4d0815d 100644 --- a/js/website.js +++ b/js/website.js @@ -196,17 +196,17 @@ function updateMenuHighlight(activePageId) { const target = link.dataset.target; if (target === activePageId) { - // 激活状态:白色粗体 - link.classList.remove('text-gray-400'); - link.classList.add('text-white', 'font-bold'); + // 激活状态:深色粗体 + link.classList.remove('text-slate-600'); + link.classList.add('text-slate-900', 'font-bold'); } else if (target === 'app') { - // 内推平台保持青色 - link.classList.remove('text-white', 'font-bold'); - link.classList.add('text-cyan-400'); + // 内推平台保持蓝色 + link.classList.remove('text-slate-900', 'font-bold'); + link.classList.add('text-blue-600'); } else { // 非激活状态:灰色 - link.classList.remove('text-white', 'font-bold'); - link.classList.add('text-gray-400'); + link.classList.remove('text-slate-900', 'font-bold'); + link.classList.add('text-slate-600'); } }); } diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..775d7d4 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,76 @@ +/** + * JWT认证中间件 + */ + +const jwt = require('jsonwebtoken'); + +const JWT_SECRET = process.env.JWT_SECRET || 'duoduo-career-jwt-secret-key-2024'; + +/** + * 验证JWT Token + */ +const verifyToken = (req, res, next) => { + try { + // 从请求头获取token + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + success: false, + message: '未提供认证令牌' + }); + } + + // 验证token + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + next(); + } catch (error) { + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + message: '令牌已过期,请重新登录' + }); + } + + return res.status(401).json({ + success: false, + message: '无效的认证令牌' + }); + } +}; + +/** + * 验证管理员权限 + */ +const verifyAdmin = (req, res, next) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ + success: false, + message: '需要管理员权限' + }); + } + next(); +}; + +/** + * 可选的Token验证(用于获取登录状态但不强制要求) + */ +const optionalAuth = (req, res, next) => { + try { + const token = req.headers.authorization?.replace('Bearer ', ''); + if (token) { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + } + } catch (error) { + // 忽略错误,继续执行 + } + next(); +}; + +module.exports = { + verifyToken, + verifyAdmin, + optionalAuth +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8730967 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1597 @@ +{ + "name": "duoduo-career-platform", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "duoduo-career-platform", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-validator": "^7.0.1", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.6.5" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.3.tgz", + "integrity": "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.15.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", + "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.0", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/validator": { + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..399f690 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "duoduo-career-platform", + "version": "1.0.0", + "description": "多多畅职大专生就业服务平台", + "main": "server.js", + "type": "commonjs", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "init-db": "node scripts/init-db.js", + "create-admin": "node scripts/create-admin.js" + }, + "keywords": [ + "career", + "jobs", + "recruitment" + ], + "author": "Duoduo Career", + "license": "MIT", + "dependencies": { + "express": "^4.18.2", + "mysql2": "^3.6.5", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express-validator": "^7.0.1" + }, + "devDependencies": { + "nodemon": "^3.0.2" + } +} diff --git a/scripts/add-professional-category.js b/scripts/add-professional-category.js new file mode 100644 index 0000000..a260a1b --- /dev/null +++ b/scripts/add-professional-category.js @@ -0,0 +1,110 @@ +/** + * 数据库迁移脚本:添加专业大类字段 + * 执行方式: node scripts/add-professional-category.js + */ + +const mysql = require('mysql2/promise'); +const fs = require('fs').promises; +const path = require('path'); + +// 数据库配置 +const dbConfig = { + host: '123.60.55.248', + port: 3306, + user: 'ddcz_bitmap', + password: 'EyTimzFHa8YfYEfY', + database: 'ddcz_bitmap' +}; + +async function runMigration() { + let connection; + + try { + console.log('🔄 连接数据库...'); + connection = await mysql.createConnection(dbConfig); + console.log('✅ 数据库连接成功\n'); + + // 执行ALTER TABLE添加字段 + console.log('🔧 执行ALTER TABLE添加专业大类字段...'); + const alterSQL = ` + ALTER TABLE \`user_profiles\` + ADD COLUMN \`professional_category\` VARCHAR(100) DEFAULT NULL COMMENT '专业大类名称' AFTER \`major\`, + ADD COLUMN \`professional_category_code\` VARCHAR(20) DEFAULT NULL COMMENT '专业大类代码' AFTER \`professional_category\` + `; + + try { + await connection.query(alterSQL); + console.log('✅ 字段添加成功\n'); + } catch (alterError) { + if (alterError.code === 'ER_DUP_FIELDNAME') { + console.log('⚠️ 字段已存在,跳过添加\n'); + } else { + throw alterError; + } + } + + // 验证字段添加 + console.log('🔍 验证字段添加...'); + const [rows] = await connection.query(` + SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'ddcz_bitmap' + AND TABLE_NAME = 'user_profiles' + AND COLUMN_NAME IN ('professional_category', 'professional_category_code') + `); + + if (rows.length === 2) { + console.log('✅ 验证成功!新增字段如下:\n'); + rows.forEach(row => { + console.log(` - ${row.COLUMN_NAME}: ${row.COLUMN_TYPE}`); + console.log(` 注释: ${row.COLUMN_COMMENT}\n`); + }); + } else { + console.log(`⚠️ 警告:预期找到2个字段,实际找到 ${rows.length} 个`); + } + + console.log('🎉 数据库迁移完成!\n'); + + // 显示当前user_profiles表的所有字段 + console.log('📋 user_profiles 表当前所有字段:\n'); + const [columns] = await connection.query(` + SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'ddcz_bitmap' + AND TABLE_NAME = 'user_profiles' + ORDER BY ORDINAL_POSITION + `); + + columns.forEach(col => { + console.log(` ${col.COLUMN_NAME.padEnd(30)} ${col.COLUMN_TYPE.padEnd(20)} ${col.COLUMN_COMMENT || ''}`); + }); + + } catch (error) { + console.error('\n❌ 迁移失败:', error.message); + + if (error.code === 'ER_DUP_FIELDNAME') { + console.log('\n💡 提示:字段已存在,无需重复添加'); + } else if (error.code === 'ECONNREFUSED') { + console.log('\n💡 提示:无法连接到数据库,请检查:'); + console.log(' 1. 数据库服务器是否运行'); + console.log(' 2. 网络连接是否正常'); + console.log(' 3. 数据库配置是否正确'); + } else { + console.log('\n💡 提示:请检查错误信息并修正后重试'); + } + + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('\n🔌 数据库连接已关闭'); + } + } +} + +// 执行迁移 +console.log('='.repeat(60)); +console.log('数据库迁移:添加专业大类字段'); +console.log('='.repeat(60)); + +runMigration(); diff --git a/scripts/add-professional-category.sql b/scripts/add-professional-category.sql new file mode 100644 index 0000000..9bb6f7e --- /dev/null +++ b/scripts/add-professional-category.sql @@ -0,0 +1,13 @@ +-- 添加专业大类字段到 user_profiles 表 +-- 执行日期: 2025-12-08 + +ALTER TABLE `user_profiles` +ADD COLUMN `professional_category` VARCHAR(100) DEFAULT NULL COMMENT '专业大类名称' AFTER `major`, +ADD COLUMN `professional_category_code` VARCHAR(20) DEFAULT NULL COMMENT '专业大类代码' AFTER `professional_category`; + +-- 验证字段添加成功 +SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = 'ddcz_bitmap' +AND TABLE_NAME = 'user_profiles' +AND COLUMN_NAME IN ('professional_category', 'professional_category_code'); diff --git a/scripts/create-admin.js b/scripts/create-admin.js new file mode 100644 index 0000000..d69825e --- /dev/null +++ b/scripts/create-admin.js @@ -0,0 +1,87 @@ +/** + * 创建默认管理员账号 + * 在数据库和表已经创建后运行此脚本 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); +const bcrypt = require('bcryptjs'); + +// 数据库配置 +const dbConfig = { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE || 'ddcz_platform' +}; + +async function createAdminUser() { + let connection; + + try { + console.log('🔌 正在连接到MySQL数据库...'); + console.log(` 数据库: ${dbConfig.database}`); + + connection = await mysql.createConnection(dbConfig); + console.log('✅ 数据库连接成功!\n'); + + // 生成管理员密码哈希 + console.log('🔐 正在生成管理员密码...'); + const defaultPassword = 'admin123456'; + const passwordHash = await bcrypt.hash(defaultPassword, 10); + console.log('✅ 密码哈希生成成功!\n'); + + // 插入或更新管理员账号 + console.log('👤 正在创建管理员账号...'); + const [result] = await connection.query(` + INSERT INTO users (username, password_hash, email, role, status) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + password_hash = VALUES(password_hash), + email = VALUES(email), + updated_at = CURRENT_TIMESTAMP + `, ['admin', passwordHash, 'admin@ddcz.com', 'admin', 'active']); + + if (result.affectedRows > 0) { + console.log('✅ 管理员账号创建/更新成功!\n'); + console.log('📝 管理员登录信息:'); + console.log(' 用户名: admin'); + console.log(' 密码: admin123456'); + console.log(' ⚠️ 请在首次登录后立即修改密码!\n'); + } + + // 验证表是否存在 + console.log('📊 验证数据库表...'); + const [tables] = await connection.query('SHOW TABLES'); + console.log(`✅ 数据库中共有 ${tables.length} 个表:`); + tables.forEach(table => { + const tableName = Object.values(table)[0]; + console.log(` - ${tableName}`); + }); + + console.log('\n🎉 管理员账号设置完成!'); + + } catch (error) { + console.error('\n❌ 操作失败:'); + console.error(error.message); + + if (error.code === 'ECONNREFUSED') { + console.error('\n💡 无法连接到MySQL服务器'); + } else if (error.code === 'ER_ACCESS_DENIED_ERROR') { + console.error('\n💡 用户名或密码错误'); + } else if (error.code === 'ER_NO_SUCH_TABLE') { + console.error('\n💡 表不存在,请先在Navicat中执行 database-setup.sql'); + } + + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('🔌 数据库连接已关闭'); + } + } +} + +// 执行 +createAdminUser(); diff --git a/scripts/create-database.sql b/scripts/create-database.sql new file mode 100644 index 0000000..7d6d052 --- /dev/null +++ b/scripts/create-database.sql @@ -0,0 +1,27 @@ +-- ======================================== +-- 多多畅职平台 - 手动创建数据库脚本 +-- ======================================== +-- 使用说明: +-- 1. 在Navicat中连接到MySQL服务器(使用root或其他管理员账号) +-- 2. 打开查询窗口,粘贴并执行此脚本 +-- 3. 执行完成后,再运行项目的 npm run init-db +-- ======================================== + +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS `ddcz_platform` +DEFAULT CHARACTER SET utf8mb4 +COLLATE utf8mb4_unicode_ci; + +-- 授予 ddcz_bitmap 用户对该数据库的所有权限 +GRANT ALL PRIVILEGES ON `ddcz_platform`.* TO 'ddcz_bitmap'@'%'; + +-- 刷新权限 +FLUSH PRIVILEGES; + +-- 验证权限 +SHOW GRANTS FOR 'ddcz_bitmap'@'%'; + +-- 提示信息 +SELECT '✅ 数据库创建成功!' AS message; +SELECT '✅ 权限授予成功!' AS message; +SELECT '📌 下一步:在项目中运行 npm run init-db' AS next_step; diff --git a/scripts/database-setup.sql b/scripts/database-setup.sql new file mode 100644 index 0000000..3cc5bb8 --- /dev/null +++ b/scripts/database-setup.sql @@ -0,0 +1,143 @@ +-- ======================================== +-- 多多畅职平台 - 完整数据库初始化SQL脚本 +-- ======================================== +-- 使用说明: +-- 1. 在Navicat中使用root或管理员账号连接MySQL +-- 2. 打开此SQL文件,点击"运行"执行整个脚本 +-- 3. 执行完成后即可使用 +-- ======================================== + +-- 创建数据库 +CREATE DATABASE IF NOT EXISTS `ddcz_platform` +DEFAULT CHARACTER SET utf8mb4 +COLLATE utf8mb4_unicode_ci; + +-- 使用数据库 +USE `ddcz_platform`; + +-- ======================================== +-- 表1: users(用户表) +-- ======================================== +CREATE TABLE IF NOT EXISTS `users` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希', + `email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱', + `phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号', + `role` ENUM('user', 'admin') NOT NULL DEFAULT 'user' COMMENT '用户角色', + `status` ENUM('active', 'inactive', 'banned') NOT NULL DEFAULT 'active' COMMENT '账号状态', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `last_login` TIMESTAMP NULL DEFAULT NULL COMMENT '最后登录时间', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_username` (`username`), + UNIQUE KEY `unique_email` (`email`), + UNIQUE KEY `unique_phone` (`phone`), + KEY `idx_username` (`username`), + KEY `idx_email` (`email`), + KEY `idx_role` (`role`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; + +-- ======================================== +-- 表2: user_profiles(用户资料表) +-- ======================================== +CREATE TABLE IF NOT EXISTS `user_profiles` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '资料ID', + `user_id` INT NOT NULL COMMENT '用户ID', + `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', + `gender` ENUM('male', 'female', 'other') DEFAULT NULL COMMENT '性别', + `birth_date` DATE DEFAULT NULL COMMENT '出生日期', + `id_card` VARCHAR(18) DEFAULT NULL COMMENT '身份证号', + `education` VARCHAR(50) DEFAULT NULL COMMENT '学历', + `major` VARCHAR(100) DEFAULT NULL COMMENT '专业', + `school` VARCHAR(100) DEFAULT NULL COMMENT '学校', + `graduation_year` YEAR DEFAULT NULL COMMENT '毕业年份', + `city` VARCHAR(50) DEFAULT NULL COMMENT '所在城市', + `address` TEXT DEFAULT NULL COMMENT '详细地址', + `avatar_url` VARCHAR(255) DEFAULT NULL COMMENT '头像URL', + `resume_url` VARCHAR(255) DEFAULT NULL COMMENT '简历URL', + `self_intro` TEXT DEFAULT NULL COMMENT '自我介绍', + `skills` TEXT DEFAULT NULL COMMENT '技能标签(JSON数组)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `unique_user_id` (`user_id`), + KEY `idx_user_id` (`user_id`), + CONSTRAINT `fk_profile_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料表'; + +-- ======================================== +-- 表3: job_applications(投递记录表) +-- ======================================== +CREATE TABLE IF NOT EXISTS `job_applications` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '投递记录ID', + `user_id` INT NOT NULL COMMENT '用户ID', + `job_type` ENUM('transition', 'referral') NOT NULL COMMENT '岗位类型:transition-过渡岗位, referral-内推岗位', + `job_name` VARCHAR(100) NOT NULL COMMENT '岗位名称', + `company_name` VARCHAR(100) NOT NULL COMMENT '企业名称', + `company_short_name` VARCHAR(50) DEFAULT NULL COMMENT '企业简称', + `city` VARCHAR(50) DEFAULT NULL COMMENT '城市', + `province` VARCHAR(50) DEFAULT NULL COMMENT '省份', + `segment_name` VARCHAR(100) DEFAULT NULL COMMENT '业务板块名称', + `status` ENUM('pending', 'reviewing', 'interviewed', 'offered', 'rejected', 'withdrawn') NOT NULL DEFAULT 'pending' COMMENT '投递状态', + `application_data` JSON DEFAULT NULL COMMENT '投递详细数据(岗位详情JSON)', + `applied_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '投递时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `notes` TEXT DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_job_type` (`job_type`), + KEY `idx_status` (`status`), + KEY `idx_applied_at` (`applied_at`), + CONSTRAINT `fk_application_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='投递记录表'; + +-- ======================================== +-- 表4: favorites(收藏表) +-- ======================================== +CREATE TABLE IF NOT EXISTS `favorites` ( + `id` INT NOT NULL AUTO_INCREMENT COMMENT '收藏ID', + `user_id` INT NOT NULL COMMENT '用户ID', + `favorite_type` ENUM('company', 'job') NOT NULL COMMENT '收藏类型', + `company_name` VARCHAR(100) DEFAULT NULL COMMENT '企业名称', + `job_name` VARCHAR(100) DEFAULT NULL COMMENT '岗位名称', + `favorite_data` JSON DEFAULT NULL COMMENT '收藏详细数据', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_type` (`favorite_type`), + UNIQUE KEY `unique_favorite` (`user_id`, `favorite_type`, `company_name`, `job_name`), + CONSTRAINT `fk_favorite_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收藏表'; + +-- ======================================== +-- 插入默认管理员账号 +-- ======================================== +-- 注意:密码哈希需要在Node.js中生成 +-- 默认密码:admin123456 +-- 这里使用bcrypt哈希后的值(rounds=10) +INSERT INTO `users` (`username`, `password_hash`, `email`, `role`, `status`) +VALUES ( + 'admin', + '$2a$10$YourBcryptHashWillBeGeneratedByNodeJS', + 'admin@ddcz.com', + 'admin', + 'active' +) ON DUPLICATE KEY UPDATE `username` = `username`; + +-- ======================================== +-- 授予权限(如果需要) +-- ======================================== +-- 如果您使用的是root账号创建的数据库,需要给 ddcz_bitmap 授权 +-- GRANT ALL PRIVILEGES ON `ddcz_platform`.* TO 'ddcz_bitmap'@'%'; +-- FLUSH PRIVILEGES; + +-- ======================================== +-- 验证表创建 +-- ======================================== +SELECT '✅ 数据库创建完成!' AS message; +SHOW TABLES; +SELECT '✅ 以上是已创建的表' AS message; + +-- 查看users表结构 +DESC `users`; diff --git a/scripts/fix-database-locks.js b/scripts/fix-database-locks.js new file mode 100644 index 0000000..cef16ea --- /dev/null +++ b/scripts/fix-database-locks.js @@ -0,0 +1,117 @@ +/** + * 修复数据库锁定问题 - 简化版 + * 分步骤执行,避免复杂查询导致临时表满 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function fixDatabaseLocks() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库'); + + // 步骤1: 查找并终止所有waiting for handler commit的进程 + console.log('\n📋 步骤1: 查找锁定的进程...'); + const [processes] = await connection.query( + "SELECT Id, State, Info FROM INFORMATION_SCHEMA.PROCESSLIST WHERE State LIKE '%waiting for handler commit%'" + ); + + console.log(`找到 ${processes.length} 个锁定的进程`); + + let killedCount = 0; + for (const p of processes) { + try { + console.log(` 正在终止进程 ${p.Id}...`); + await connection.query(`KILL ${p.Id}`); + console.log(` ✅ 已终止进程 ${p.Id}`); + killedCount++; + } catch (err) { + console.log(` ⚠️ 无法终止进程 ${p.Id}:`, err.message); + } + } + + console.log(`\n✅ 成功终止 ${killedCount} 个锁定的进程`); + + // 步骤2: 终止所有Sleep状态的连接 + console.log('\n📋 步骤2: 终止Sleep状态的连接...'); + const [allProcesses] = await connection.query('SHOW FULL PROCESSLIST'); + + let sleepKilled = 0; + for (const p of allProcesses) { + if (p.Command === 'Sleep' && p.Id !== connection.threadId) { + try { + await connection.query(`KILL ${p.Id}`); + console.log(` ✅ 已终止Sleep进程 ${p.Id}`); + sleepKilled++; + } catch (err) { + console.log(` ⚠️ 无法终止进程 ${p.Id}:`, err.message); + } + } + } + + console.log(`\n✅ 成功终止 ${sleepKilled} 个Sleep连接`); + + // 步骤3: 检查是否有重复的确认记录(使用简单查询) + console.log('\n📋 步骤3: 检查重复记录...'); + const [allConfirmations] = await connection.query( + 'SELECT id, user_id, training_unit_id FROM training_confirmations ORDER BY user_id, training_unit_id, id' + ); + + console.log(`总共有 ${allConfirmations.length} 条确认记录`); + + // 找出重复的记录(在应用层处理,避免复杂SQL) + const seen = new Map(); + const duplicates = []; + + for (const record of allConfirmations) { + const key = `${record.user_id}_${record.training_unit_id}`; + if (seen.has(key)) { + // 这是重复的,标记为要删除 + duplicates.push(record.id); + } else { + // 第一次见到,保留 + seen.set(key, record.id); + } + } + + if (duplicates.length > 0) { + console.log(`\n发现 ${duplicates.length} 条重复记录,正在删除...`); + for (const id of duplicates) { + await connection.query('DELETE FROM training_confirmations WHERE id = ?', [id]); + console.log(` ✅ 已删除重复记录 ID: ${id}`); + } + } else { + console.log('✅ 没有发现重复记录'); + } + + // 步骤4: 解锁所有表 + console.log('\n📋 步骤4: 解锁所有表...'); + await connection.query('UNLOCK TABLES'); + console.log('✅ 表已解锁'); + + console.log('\n✅ 所有操作完成!'); + console.log('\n💡 现在可以重新测试预招录确认功能了'); + console.log('\n建议:如果问题仍然存在,可能需要重启Node.js服务器以释放连接池'); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + console.error('错误详情:', error); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +fixDatabaseLocks(); diff --git a/scripts/fix-locked-transaction.js b/scripts/fix-locked-transaction.js new file mode 100644 index 0000000..952739b --- /dev/null +++ b/scripts/fix-locked-transaction.js @@ -0,0 +1,88 @@ +/** + * 修复锁定的事务 + * 强制终止等待提交的进程 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function fixLockedTransaction() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库'); + + // 1. 查找所有waiting for handler commit的进程 + console.log('\n📋 查找锁定的进程...'); + const [processes] = await connection.query( + "SELECT Id, State, Info FROM INFORMATION_SCHEMA.PROCESSLIST WHERE State LIKE '%waiting for handler commit%'" + ); + + console.log(`找到 ${processes.length} 个锁定的进程`); + + // 2. 终止所有锁定的进程 + let killedCount = 0; + for (const p of processes) { + try { + console.log(` 正在终止进程 ${p.Id}: ${p.Info?.substring(0, 100)}...`); + await connection.query(`KILL ${p.Id}`); + console.log(` ✅ 已终止进程 ${p.Id}`); + killedCount++; + } catch (err) { + console.log(` ⚠️ 无法终止进程 ${p.Id}:`, err.message); + } + } + + console.log(`\n✅ 成功终止 ${killedCount} 个锁定的进程`); + + // 3. 清理可能重复的确认记录 + console.log('\n🧹 清理可能的重复记录...'); + const [duplicates] = await connection.query(` + SELECT user_id, training_unit_id, COUNT(*) as count + FROM training_confirmations + GROUP BY user_id, training_unit_id + HAVING count > 1 + `); + + if (duplicates.length > 0) { + console.log(`发现 ${duplicates.length} 组重复记录,正在清理...`); + for (const dup of duplicates) { + // 保留最早的记录,删除其他 + await connection.query(` + DELETE FROM training_confirmations + WHERE user_id = ? AND training_unit_id = ? + AND id NOT IN ( + SELECT id FROM ( + SELECT MIN(id) as id FROM training_confirmations + WHERE user_id = ? AND training_unit_id = ? + ) as temp + ) + `, [dup.user_id, dup.training_unit_id, dup.user_id, dup.training_unit_id]); + console.log(` ✅ 清理了 user_id=${dup.user_id}, training_unit_id=${dup.training_unit_id} 的重复记录`); + } + } else { + console.log('没有发现重复记录'); + } + + console.log('\n✅ 所有操作完成!'); + console.log('\n💡 现在可以重新测试预招录确认功能了'); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +fixLockedTransaction(); diff --git a/scripts/grant-permissions.sql b/scripts/grant-permissions.sql new file mode 100644 index 0000000..6c0cf77 --- /dev/null +++ b/scripts/grant-permissions.sql @@ -0,0 +1,18 @@ +-- ======================================== +-- 授予 ddcz_bitmap 用户对 ddcz_platform 数据库的权限 +-- ======================================== +-- 请使用 root 或管理员账号在 Navicat 中执行此脚本 +-- ======================================== + +-- 授予 ddcz_bitmap 对 ddcz_platform 数据库的所有权限 +GRANT ALL PRIVILEGES ON `ddcz_platform`.* TO 'ddcz_bitmap'@'%'; + +-- 刷新权限,使其立即生效 +FLUSH PRIVILEGES; + +-- 验证权限是否授予成功 +SHOW GRANTS FOR 'ddcz_bitmap'@'%'; + +-- 提示信息 +SELECT '✅ 权限授予成功!' AS message; +SELECT '📌 现在可以运行: node scripts/test-permissions.js' AS next_step; diff --git a/scripts/init-db.js b/scripts/init-db.js new file mode 100644 index 0000000..5fe6862 --- /dev/null +++ b/scripts/init-db.js @@ -0,0 +1,209 @@ +/** + * 数据库初始化脚本 + * 创建数据库和表结构 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +// 数据库配置 +const dbConfig = { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD +}; + +// 数据库名称 +const DATABASE_NAME = process.env.DB_DATABASE || 'ddcz_platform'; + +async function initDatabase() { + let connection; + + try { + console.log('🔌 正在连接到MySQL服务器...'); + console.log(` 主机: ${dbConfig.host}:${dbConfig.port}`); + console.log(` 用户: ${dbConfig.user}`); + + // 连接到MySQL服务器(不指定数据库) + connection = await mysql.createConnection(dbConfig); + console.log('✅ MySQL连接成功!\n'); + + // 创建数据库(如果不存在) + console.log(`📦 创建数据库: ${DATABASE_NAME}...`); + await connection.query(`CREATE DATABASE IF NOT EXISTS \`${DATABASE_NAME}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); + console.log('✅ 数据库创建成功!\n'); + + // 切换到目标数据库 + await connection.query(`USE \`${DATABASE_NAME}\``); + + // ======================================== + // 清理旧表(如果存在) + // ======================================== + console.log('🧹 清理可能存在的旧表...'); + await connection.query('DROP TABLE IF EXISTS `ddcz_platform`'); + await connection.query('DROP TABLE IF EXISTS `favorites`'); + await connection.query('DROP TABLE IF EXISTS `job_applications`'); + await connection.query('DROP TABLE IF EXISTS `user_profiles`'); + await connection.query('DROP TABLE IF EXISTS `users`'); + console.log('✅ 旧表清理完成!\n'); + + // ======================================== + // 创建 users 表 - 用户基本信息 + // ======================================== + console.log('📋 创建 users 表...'); + await connection.query(` + CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID', + username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希', + email VARCHAR(100) UNIQUE COMMENT '邮箱', + phone VARCHAR(20) UNIQUE COMMENT '手机号', + role ENUM('user', 'admin') DEFAULT 'user' COMMENT '用户角色', + status ENUM('active', 'inactive', 'banned') DEFAULT 'active' COMMENT '账号状态', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + last_login TIMESTAMP NULL COMMENT '最后登录时间', + INDEX idx_username (username), + INDEX idx_email (email), + INDEX idx_role (role) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表' + `); + console.log('✅ users 表创建成功!'); + + // ======================================== + // 创建 user_profiles 表 - 用户详细资料 + // ======================================== + console.log('📋 创建 user_profiles 表...'); + await connection.query(` + CREATE TABLE IF NOT EXISTS user_profiles ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '资料ID', + user_id INT NOT NULL UNIQUE COMMENT '用户ID', + real_name VARCHAR(50) COMMENT '真实姓名', + gender ENUM('male', 'female', 'other') COMMENT '性别', + birth_date DATE COMMENT '出生日期', + id_card VARCHAR(18) COMMENT '身份证号', + education VARCHAR(50) COMMENT '学历', + major VARCHAR(100) COMMENT '专业', + school VARCHAR(100) COMMENT '学校', + graduation_year YEAR COMMENT '毕业年份', + city VARCHAR(50) COMMENT '所在城市', + address TEXT COMMENT '详细地址', + avatar_url VARCHAR(255) COMMENT '头像URL', + resume_url VARCHAR(255) COMMENT '简历URL', + self_intro TEXT COMMENT '自我介绍', + skills TEXT COMMENT '技能标签(JSON数组)', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_user_id (user_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料表' + `); + console.log('✅ user_profiles 表创建成功!'); + + // ======================================== + // 创建 job_applications 表 - 投递记录 + // ======================================== + console.log('📋 创建 job_applications 表...'); + await connection.query(` + CREATE TABLE IF NOT EXISTS job_applications ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '投递记录ID', + user_id INT NOT NULL COMMENT '用户ID', + job_type ENUM('transition', 'referral') NOT NULL COMMENT '岗位类型:transition-过渡岗位, referral-内推岗位', + job_name VARCHAR(100) NOT NULL COMMENT '岗位名称', + company_name VARCHAR(100) NOT NULL COMMENT '企业名称', + company_short_name VARCHAR(50) COMMENT '企业简称', + city VARCHAR(50) COMMENT '城市', + province VARCHAR(50) COMMENT '省份', + segment_name VARCHAR(100) COMMENT '业务板块名称', + status ENUM('pending', 'reviewing', 'interviewed', 'offered', 'rejected', 'withdrawn') DEFAULT 'pending' COMMENT '投递状态', + application_data JSON COMMENT '投递详细数据(岗位详情JSON)', + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '投递时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + notes TEXT COMMENT '备注', + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_user_id (user_id), + INDEX idx_job_type (job_type), + INDEX idx_status (status), + INDEX idx_applied_at (applied_at) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='投递记录表' + `); + console.log('✅ job_applications 表创建成功!'); + + // ======================================== + // 创建 favorites 表 - 收藏夹 + // ======================================== + console.log('📋 创建 favorites 表...'); + await connection.query(` + CREATE TABLE IF NOT EXISTS favorites ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT '收藏ID', + user_id INT NOT NULL COMMENT '用户ID', + favorite_type ENUM('company', 'job') NOT NULL COMMENT '收藏类型', + company_name VARCHAR(100) COMMENT '企业名称', + job_name VARCHAR(100) COMMENT '岗位名称', + favorite_data JSON COMMENT '收藏详细数据', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + INDEX idx_user_id (user_id), + INDEX idx_type (favorite_type), + UNIQUE KEY unique_favorite (user_id, favorite_type, company_name, job_name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收藏表' + `); + console.log('✅ favorites 表创建成功!'); + + // ======================================== + // 创建默认管理员账号 + // ======================================== + console.log('\n👤 创建默认管理员账号...'); + const bcrypt = require('bcryptjs'); + const defaultPassword = 'admin123456'; + const passwordHash = await bcrypt.hash(defaultPassword, 10); + + await connection.query(` + INSERT INTO users (username, password_hash, email, role, status) + VALUES ('admin', ?, 'admin@ddcz.com', 'admin', 'active') + ON DUPLICATE KEY UPDATE username=username + `, [passwordHash]); + + console.log('✅ 默认管理员账号创建成功!'); + console.log(' 用户名: admin'); + console.log(' 密码: admin123456'); + console.log(' ⚠️ 请在首次登录后立即修改密码!\n'); + + // ======================================== + // 显示表结构信息 + // ======================================== + console.log('📊 数据库表统计:'); + const [tables] = await connection.query('SHOW TABLES'); + console.log(` 共创建 ${tables.length} 个表:`); + tables.forEach(table => { + const tableName = Object.values(table)[0]; + console.log(` - ${tableName}`); + }); + + console.log('\n🎉 数据库初始化完成!'); + + } catch (error) { + console.error('\n❌ 数据库初始化失败:'); + console.error(error.message); + + if (error.code === 'ECONNREFUSED') { + console.error('\n💡 提示:无法连接到MySQL服务器,请检查:'); + console.error(' 1. MySQL服务器是否正在运行'); + console.error(' 2. 主机地址和端口是否正确'); + console.error(' 3. 防火墙是否允许连接'); + } else if (error.code === 'ER_ACCESS_DENIED_ERROR') { + console.error('\n💡 提示:用户名或密码错误,请检查.env文件中的配置'); + } + + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('🔌 数据库连接已关闭'); + } + } +} + +// 执行初始化 +initDatabase(); diff --git a/scripts/init-high-page-data.sql b/scripts/init-high-page-data.sql new file mode 100644 index 0000000..4150f28 --- /dev/null +++ b/scripts/init-high-page-data.sql @@ -0,0 +1,341 @@ +-- ========================================== +-- 高薪岗位页面数据库初始化脚本 +-- 基于high.html的最新数据结构 +-- 包含所有测试数据 +-- ========================================== + +USE ddcz_bitmap; + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ========================================== +-- 1. 创建高薪岗位表 +-- ========================================== +DROP TABLE IF EXISTS high_salary_jobs; +CREATE TABLE high_salary_jobs ( + id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(100) NOT NULL COMMENT '职位名称', + salary VARCHAR(50) NOT NULL COMMENT '薪资范围', + company VARCHAR(200) NOT NULL COMMENT '公司名称', + location VARCHAR(100) NOT NULL COMMENT '工作地点', + quota INT NOT NULL DEFAULT 0 COMMENT '招聘人数', + tags JSON COMMENT '福利标签数组', + requirements TEXT COMMENT '岗位要求', + description TEXT COMMENT '企业介绍', + icon VARCHAR(50) DEFAULT 'fa-microchip' COMMENT '图标类名', + is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用', + display_order INT DEFAULT 0 COMMENT '显示顺序', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_location (location), + INDEX idx_is_active (is_active), + INDEX idx_display_order (display_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='高薪岗位信息表'; + +-- ========================================== +-- 2. 创建培训单元表 +-- ========================================== +DROP TABLE IF EXISTS training_units; +CREATE TABLE training_units ( + id INT PRIMARY KEY AUTO_INCREMENT, + title VARCHAR(200) NOT NULL COMMENT '课程标题', + start_date DATE NOT NULL COMMENT '开课时间', + related_positions JSON COMMENT '对应岗位列表', + related_companies JSON COMMENT '相关企业列表', + status ENUM('locked', 'active') DEFAULT 'locked' COMMENT '单元状态', + display_order INT DEFAULT 0 COMMENT '显示顺序', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_start_date (start_date), + INDEX idx_status (status), + INDEX idx_display_order (display_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='培训单元信息表'; + +-- ========================================== +-- 3. 创建成功案例表 +-- ========================================== +DROP TABLE IF EXISTS success_stories; +CREATE TABLE success_stories ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL COMMENT '姓名 (脱敏)', + job VARCHAR(100) NOT NULL COMMENT '职位', + company VARCHAR(200) NOT NULL COMMENT '公司名称', + salary VARCHAR(20) NOT NULL COMMENT '薪资 (如:9k)', + avatar_color VARCHAR(20) DEFAULT '#2563eb' COMMENT '头像背景色', + track_type ENUM('fast', 'slow', 'medium') DEFAULT 'fast' COMMENT '轨道类型', + is_active BOOLEAN DEFAULT TRUE COMMENT '是否显示', + display_order INT DEFAULT 0 COMMENT '显示顺序', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_track_type (track_type), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='成功案例弹幕数据表'; + +-- ========================================== +-- 4. 创建面试进度表 +-- ========================================== +DROP TABLE IF EXISTS transition_progress; +CREATE TABLE transition_progress ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT COMMENT '用户ID (可选,关联users表)', + company VARCHAR(200) NOT NULL COMMENT '公司名称', + job VARCHAR(100) NOT NULL COMMENT '职位', + status VARCHAR(50) NOT NULL COMMENT '状态描述', + status_type ENUM('offer', 'hr', 'ing', 'pending') DEFAULT 'pending' COMMENT '状态类型', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_status_type (status_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='面试进度信息表'; + +-- ========================================== +-- 5. 创建培训单元确认表 +-- ========================================== +DROP TABLE IF EXISTS training_confirmations; +CREATE TABLE training_confirmations ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL COMMENT '用户ID', + training_unit_id INT NOT NULL COMMENT '培训单元ID', + confirmed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '确认时间', + INDEX idx_user_id (user_id), + INDEX idx_training_unit_id (training_unit_id), + UNIQUE KEY uk_user_training (user_id, training_unit_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='培训单元确认记录表'; + +-- ========================================== +-- 插入高薪岗位数据 (20条完整数据) +-- ========================================== +INSERT INTO high_salary_jobs (title, salary, company, location, quota, tags, requirements, description, icon, display_order) VALUES +('自动化技术员', '4K-6K', '恒力集团有限公司', '苏州市', 5, + '["五险一金", "年终奖", "项目奖金", "单休", "员工宿舍", "绩效奖金"]', + '1. 电气、自动化或相关专业,大专及以上学历;\n2. 熟悉 PLC/继电器/电气控制原理,能独立完成设备电气线路安装与调试;\n3. 有自动化设备维护、故障分析经验者优先;\n4. 工作认真负责,有团队协作精神。', + '恒力集团有限公司成立于1994年,总部位于江苏省苏州市吴江区,是立足实业发展的全球化民营企业。集团注册资产达520亿元人民币,全球员工总数超12万人,2022年实现营业收入6117亿元,连续七年跻身世界500强企业榜单。\n公司以石化、聚酯新材料、纺织为主业,构建形成"原油-芳烃-乙烯-精对苯二甲酸(PTA)-聚酯-纺织"全产业链布局。', + 'fa-microchip', 1), + +('自动化技术员', '6-8K', '东莞市宇瞳光学科技股份有限公司', '东莞市', 10, + '["住宿/宿舍", "五险一金", "定期体检", "加班费依法支付"]', + '1. 男女不限。\n2. 大专或高职以上学历,25届/26届实习+就业的实习生。\n3. 光学光电、机械及自动化、机电一体化、工业机器人、电气及自动化等理工科专业。\n4. 能吃苦耐劳、服从安排、具备良好的沟通能力及团队合作精神。\n5. 能接受实习期深入基层、扎根基础岗位学习各类自动化工艺。', + '东莞市宇瞳光学科技股份有限公司成立于2011年9月,是一家专营光学产品的高新技术企业,占地面积15万平方米,月镜头生产能力1000万多支,年产值10亿元,现有员工2500多人。\n公司主要经营光学精密镜片、光学镜头、光学仪器等相关产品的研发、生产、销售和售后服务。', + 'fa-microchip', 2), + +('工业机器人调试工程师', '8-10K', '爱仕达股份有限公司', '温州', 15, + '["项目奖金", "出差补贴", "定期体检", "交通补贴"]', + '1. 熟悉机器人焊接、搬运、码垛等工艺的调试;\n2. 熟悉ABB/KUKA/FANUC机器人优先;\n3. 具备现场项目管理能力和技术沟通能力;\n4. 熟悉机器人编程语言,了解工业机器人的结构特点;\n5. 熟悉电气控制系统原理,IO通讯、总线系统、PLC原理;\n6. 学历:专科及以上学历,可接受能力优秀的应届生。', + '爱仕达股份有限公司创立于1978年,是一家集炊具、厨房小家电、工业机器人研发、制造、营销为一体的上市企业(股票代码:002403)。员工近10000人, 2021年公司销售收入30多亿。\n公司是不粘锅、无油烟锅、铝压力锅、不锈钢压力锅、电压力锅等20项与炊具产业相关的国家或行业标准起草单位。', + 'fa-robot', 3), + +('非标自动化工程师', '7-10k', '光深自动化有限公司', '西安', 20, + '["五险一金", "年终奖", "项目奖金", "单休", "员工宿舍", "绩效奖金"]', + '1. 机械工程或相关专业背景,具备扎实的机械设计基础;\n2. 熟练掌握3D建模软件(如SolidWorks、AutoCAD)及2D绘图能力;\n3. 拥有良好的项目管理能力和沟通技巧,能有效协调跨部门合作;\n4. 对非标自动化设备有深刻理解和实践经验,具备较强的分析和解决复杂问题的能力。', + '光深自动化有限公司专注于工业自动化系统设计开发、自动化控制器等产品研发制造及智能家居系统开发,秉持创新理念,为客户提供高品质自动化产品与解决方案,并致力于推动行业发展。', + 'fa-microchip', 4), + +('自动化设备调试工程师', '7-9k', '深圳市标王工业设备有限公司', '东莞', 25, + '["差旅报销/住宿安排", "出差补贴", "项目奖金", "夜班津贴"]', + '1. 具备强烈的问题解决能力,能够独立处理和解决设备调试过程中遇到的问题。\n2. 拥有良好的团队合作精神,能与团队成员有效沟通。\n3. 具备良好的动手能力,能够进行设备装配和调试。\n4. 能够熟练使用相关调试工具和设备。\n5. 有较强的学习能力和技术研究能力。', + '深圳市标王工业设备有限公司是中国领先的半导体封测设备、半导体3D检测设备的研发、设计、生产、销售和服务于一体的中大型高科技企业。\n公司创建于2009年,经过十多年的拼搏已成为半导体分选设备及老化设备领域行业领先、技术先进的高新技术企业。', + 'fa-screwdriver-wrench', 5), + +('自动化调试工程师', '6-10K', '苏州科林利华电子有限公司', '苏州', 5, + '["带薪年假", "年终奖", "五险一金"]', + '1. 负责非标自动化设备的电气调试和维护;\n2. 能够阅读电气原理图,熟练接线;\n3. 熟悉各种传感器、伺服、步进电机的使用;\n4. 适应出差,有较强的责任心。', + '苏州科林利华电子有限公司成立于2010年,是一家专注于自动化设备研发和制造的高科技企业。公司主要产品包括自动组装机、检测设备、包装设备等,服务于消费电子、汽车零部件等行业。', + 'fa-screwdriver-wrench', 6), + +('机器人应用工程师', '5-8K', '佛山华数机器人有限公司', '佛山', 10, + '["五险一金", "定期体检", "带薪年假", "通讯补贴"]', + '1. 机械、电气自动化等相关专业,大专及以上学历;\n2. 熟悉工业机器人的操作和编程,如华数、FANUC、ABB等;\n3. 具备良好的电气基础知识,能看懂电气图纸;\n4. 有机器人焊接、打磨、搬运项目经验者优先;\n5. 工作积极主动,具备良好的团队协作能力。', + '佛山华数机器人有限公司是武汉华中数控股份有限公司旗下子公司,专注于工业机器人产品的研发、制造和销售。公司产品涵盖多关节机器人、SCARA机器人、Delta机器人等,广泛应用于焊接、打磨、搬运、装配等领域。', + 'fa-robot', 7), + +('自动化工程师', '10-15K', '成都宏明双新科技股份有限公司', '成都', 15, + '["六险一金", "带薪年假", "餐补", "定期体检"]', + '1. 本科及以上学历,自动化、测控等相关专业;\n2. 3年以上自动化设备设计开发经验;\n3. 熟练掌握PLC编程(西门子、三菱等);\n4. 熟悉上位机软件开发(C#、LabVIEW等);\n5. 具备良好的项目管理能力和英语阅读能力。', + '成都宏明双新科技股份有限公司成立于2000年,主要从事精密模具、精密冲压件、注塑件及自动化设备的研发、生产和销售。公司是国家高新技术企业,产品广泛应用于3C电子、汽车电子等领域。', + 'fa-microchip', 8), + +('自动化调试工程师', '5-8K', '湖南德沃智能制造有限公司', '长沙', 20, + '["包吃住", "节日福利", "专业培训"]', + '1. 自动化类相关专业,中专及以上学历;\n2. 了解PLC控制原理,认识常用电气元件;\n3. 动手能力强,能吃苦耐劳;\n4. 适应倒班和加班,有志于在自动化行业长期发展。', + '湖南德沃智能制造有限公司是一家集研发、生产、销售为一体的智能装备制造企业。公司主要产品包括自动化装配线、检测设备、机器人工作站等。公司注重人才培养,为员工提供良好的晋升通道和发展空间。', + 'fa-screwdriver-wrench', 9), + +('电气自动化工程师', '7-9K', '江西联创电子有限公司', '南昌', 25, + '["五险一金", "年终奖", "股票期权", "带薪年假"]', + '1. 电气工程及其自动化专业,本科及以上学历;\n2. 熟悉PLC、触摸屏、伺服驱动器的应用;\n3. 能独立完成小型自动化项目的电气设计和调试;\n4. 熟练使用AutoCAD或EPLAN绘制电气图纸;\n5. 具备良好的沟通能力和学习能力。', + '江西联创电子有限公司是联创电子科技股份有限公司(股票代码:002036)的全资子公司,主要从事光学镜头、摄像模组及触控显示一体化产品的研发、生产和销售。公司产品广泛应用于智能手机、平板电脑、智能汽车等领域。', + 'fa-microchip', 10), + +('设备调试工程师', '8-12K', '宁德时代新能源科技股份有限公司', '宁德', 5, + '["五险一金", "补充医疗保险", "定期体检", "免费班车"]', + '1. 大专及以上学历,机械、电气自动化相关专业;\n2. 2年以上锂电设备调试经验;\n3. 熟悉涂布机、卷绕机、化成设备等锂电前中后段设备;\n4. 具备较强的故障排查和分析能力;\n5. 适应无尘车间工作环境。', + '宁德时代新能源科技股份有限公司(CATL)是全球领先的新能源创新科技公司,致力于为全球新能源应用提供一流解决方案和服务。公司主要从事动力电池及储能电池的研发、生产和销售。', + 'fa-screwdriver-wrench', 11), + +('自动化助理工程师', '5-7K', '郑州富士康科技集团', '郑州', 10, + '["包吃住", "五险一金", "带薪年假", "加班补助"]', + '1. 大专及以上学历,机电一体化、自动化相关专业;\n2. 了解基本的电路知识和机械原理;\n3. 熟练使用办公软件;\n4. 工作细致认真,服从安排;\n5. 优秀应届毕业生亦可。', + '郑州富士康科技集团是鸿海精密集团在大陆投资兴建的专业研发制造基地,主要从事智能手机等电子产品的生产。公司拥有先进的生产设备和完善的管理体系,是全球最大的智能手机生产基地之一。', + 'fa-microchip', 12), + +('机器人维护工程师', '6-9K', '杭州海康威视数字技术股份有限公司', '杭州', 15, + '["五险一金", "补充医疗保险", "定期体检", "交通补贴"]', + '1. 专科及以上学历,工业机器人、自动化等相关专业;\n2. 熟悉海康机器人或其他品牌机器人的操作和维护;\n3. 具备一定的电气故障排查能力;\n4. 了解机器视觉系统者优先;\n5. 有良好的服务意识和团队合作精神。', + '杭州海康威视数字技术股份有限公司是全球领先的以视频为核心的物联网解决方案提供商。公司致力于通过持续创新,为客户提供高质量的产品和服务。海康机器人是海康威视旗下的机器视觉和移动机器人业务单元。', + 'fa-robot', 13), + +('PLC自动化工程师', '7-10K', '厦门盈趣科技股份有限公司', '厦门', 20, + '["五险一金", "年终奖", "股票期权", "带薪年假"]', + '1. 本科及以上学历,自动化、电子信息等相关专业;\n2. 精通西门子或欧姆龙PLC编程;\n3. 熟悉工业网络通信协议(Profinet, EtherCAT等);\n4. 有非标自动化设备电气设计经验者优先;\n5. 具备良好的英语读写能力。', + '厦门盈趣科技股份有限公司成立于2011年,致力于成为"工业互联网"和"民用物联网"领域的领军企业。公司主要为客户提供智能控制部件、创新消费电子等产品的研发、生产和销售。', + 'fa-microchip', 14), + +('产线调试技术员', '6-8K', '重庆长安汽车股份有限公司', '重庆', 25, + '["五险一金", "免费班车", "定期体检", "高温补贴"]', + '1. 中专及以上学历,汽车维修、机电一体化相关专业;\n2. 熟悉汽车总装工艺流程;\n3. 掌握常用的汽车检测设备和工具的使用;\n4. 具备一定的汽车故障诊断和排除能力;\n5. 吃苦耐劳,适应倒班工作。', + '重庆长安汽车股份有限公司是中国汽车行业的骨干企业,拥有160年的历史底蕴。公司主要从事汽车及发动机的研发、制造和销售。长安汽车始终坚持自主创新,致力于打造世界一流的汽车企业。', + 'fa-screwdriver-wrench', 15), + +('自动化系统集成工程师', '9-13K', '青岛海尔集团', '青岛', 5, + '["五险一金", "补充医疗保险", "年终奖", "员工旅游"]', + '1. 本科及以上学历,自动化、计算机等相关专业;\n2. 熟悉SCADA系统、MES系统的开发和应用;\n3. 掌握SQL Server或Oracle数据库;\n4. 有智能工厂系统集成项目经验者优先;\n5. 具备良好的逻辑思维能力和解决问题的能力。', + '海尔集团是全球领先的美好生活解决方案服务商。公司在物联网时代,致力于转型为物联网生态品牌。海尔智家是海尔集团旗下上市公司,提供全场景智慧家庭解决方案。公司拥有全球领先的互联工厂,积极探索工业4.0和智能制造的实践。', + 'fa-microchip', 16), + +('工业机器人现场工程师', '8-12K', '埃斯顿自动化股份有限公司', '南京', 10, + '["五险一金", "带薪年假", "定期体检", "通讯补贴"]', + '1. 大专及以上学历,机电一体化、机器人工程专业;\n2. 熟悉埃斯顿机器人产品特性及编程;\n3. 能适应长期出差,在客户现场进行调试和培训;\n4. 具备良好的沟通协调能力和抗压能力;\n5. 有焊接、折弯机器人应用经验者优先。', + '埃斯顿自动化股份有限公司是中国工业机器人行业的领军企业之一。公司形成了"核心部件+本体+工业机器人集成应用"的全产业链竞争优势。产品广泛应用于光伏、锂电、金属加工等行业。', + 'fa-robot', 17), + +('电气控制技术员', '6-9K', '天津长城汽车哈弗分公司', '天津', 15, + '["五险一金", "免费工作餐", "提供住宿", "年终奖"]', + '1. 大专学历,电气自动化相关专业;\n2. 熟悉西门子S7-1200/1500 PLC;\n3. 了解变频器、伺服驱动器的参数设置;\n4. 具备电气柜配盘和现场布线能力;\n5. 工作踏实,服从管理。', + '天津长城汽车哈弗分公司是长城汽车在天津的重要生产基地,主要生产哈弗品牌SUV车型。工厂拥有冲压、焊装、涂装、总装四大工艺车间,自动化程度高。公司为员工提供广阔的发展平台和完善的薪酬福利体系。', + 'fa-microchip', 18), + +('设备维护调试员', '7-10K', '合肥京东方光电科技有限公司', '合肥', 20, + '["六险二金", "提供住宿", "免费班车", "带薪年假"]', + '1. 大专及以上学历,机械、电子、自动化相关专业;\n2. 有液晶面板厂设备维护经验者优先;\n3. 熟悉气动元件、真空设备、传动机构的维护;\n4. 具备基本的PLC故障诊断能力;\n5. 适应倒班工作制。', + '京东方科技集团股份有限公司(BOE)是全球领先的半导体显示技术、产品与服务提供商。合肥京东方拥有多条高世代TFT-LCD生产线,是全球重要的显示面板生产基地。公司坚持创新驱动,不仅在显示领域保持领先,还在智慧系统、健康服务等领域积极布局。', + 'fa-screwdriver-wrench', 19), + +('初级自动化技术员', '5-8K', '昆山龙腾光电股份有限公司', '昆山', 25, + '["五险一金", "加班补助", "包吃住", "节日福利"]', + '1. 中专或高中以上学历,理工科背景;\n2. 对自动化设备感兴趣,愿意学习新技术;\n3. 具备一定的动手能力;\n4. 身体健康,视力良好;\n5. 欢迎应届毕业生加入。', + '昆山龙腾光电股份有限公司成立于2005年,是国内知名的液晶显示面板制造商。公司主要产品为笔记本电脑、车载显示、手机等中小尺寸显示面板。公司位于江苏省昆山市光电产业园,拥有先进的生产技术和严格的质量管理体系。', + 'fa-microchip', 20); + + +-- ========================================== +-- 插入培训单元数据 (8条完整数据) +-- ========================================== +INSERT INTO training_units (title, start_date, related_positions, related_companies, display_order) VALUES +('SOLIDWORKS与钣金设计类岗位', '2026-04-20', + '["焊接工艺工程师", "焊接技术员", "钣金工程师"]', + '["吉利控股集团有限公司", "保定成聚模具冲压有限公司", "佳创机械设备制造(固安)有限公司", "浙江华岳包装机械有限公司", "江阴兴澄特种钢铁有限公司", "江苏交通控股有限公司", "徐州工程机械集团有限公司"]', + 1), + +('UG编程与CNC编程类岗位', '2026-04-25', + '["CNC编程工程师", "刀具应用工程师", "刀具技术工程师", "CNC技术员", "CNC调试员"]', + '["盛虹控股集团有限公司", "江苏沙钢集团有限公司", "协鑫集团有限公司", "中天钢铁集团有限公司", "江苏新长江实业集团有限公司", "苏美达股份有限公司"]', + 2), + +('模具设计类岗位必备核心技能', '2026-05-05', + '["模具设计师", "模具质检员", "模具质量工程师", "模具工", "模具工程师"]', + '["江苏江中集团有限公司", "无锡江南电缆有限公司", "江苏长电科技股份有限公司", "江苏扬子江船业集团有限公司", "中车唐山机车车辆有限公司"]', + 3), + +('快速上手PLC编程调试类岗位', '2026-05-11', + '["PLC编程工程师", "电气控制工程师", "PLC自动化工程师", "PLC工程师", "电气自动化管培生"]', + '["上海汽车集团股份有限公司", "中国石化上海石油化工股份有限公司", "上海华谊控股集团有限公司", "上海通用电焊机股份有限公司"]', + 4), + +('工业机器人运维调试类岗位实战技巧', '2026-05-18', + '["工业机器人工程师", "工业机器人调试工程师", "工业机器人调试技术员", "工业机器人工程师助理"]', + '["奇瑞控股集团有限公司", "华勤技术股份有限公司", "蔚来集团", "美的集团股份有限公司", "TCL科技集团股份有限公司", "立讯精密工业股份有限公司"]', + 5), + +('机器识别类岗位上岗必学技能', '2026-05-25', + '["视觉检测工程师", "机器视觉工程师", "机器视觉应用工程师", "机器视觉调试技术员", "机器视觉调试工程师"]', + '["中天科技集团有限公司", "立铠精密科技(盐城)有限公司", "远景能源有限公司", "天合光能股份有限公司", "惠州亿纬锂能股份有限公司", "广东伊之密精密机械股份有限公司"]', + 6), + +('C#与上位机;高阶岗位技能', '2026-06-01', + '["工业机器人工程师", "PLC自动化工程师", "自动化工程师", "通信协议工程师", "机器视觉应用工程师"]', + '["恒力集团有限公司", "亨通集团有限公司", "永鼎集团有限公司", "江苏沃得机电集团有限公司", "重庆长线智能科技有限责任公司", "宁波均胜电子股份有限公司"]', + 7), + +('非标自动化实战与行业发展精进', '2026-06-08', + '["非标自动化工艺工程师", "非标自动化结构工程师", "非标设备电气工程师", "非标自动化工程师"]', + '["宿迁阿特斯阳光能源科技有限公司", "重庆钢铁股份有限公司", "重庆宗申发动机制造有限公司", "重庆美利信科技股份有限公司", "重庆京东方光电科技有限公司", "江苏恒瑞医药股份有限公司"]', + 8); + +-- ========================================== +-- 插入成功案例数据 (35条完整数据) +-- ========================================== + +-- fast轨道 (12条) +INSERT INTO success_stories (name, job, company, salary, avatar_color, track_type, display_order) VALUES +('赵**', '自动化技术员', '恒力集团有限公司', '9k', '#2563eb', 'fast', 1), +('钱**', '工业机器人调试工程师', '亨通集团有限公司', '11k', '#db2777', 'fast', 2), +('孙**', '非标自动化工程师', '盛虹控股集团有限公司', '10k', '#7c3aed', 'fast', 3), +('李**', '自动化设备调试工程师', '江苏沙钢集团有限公司', '12k', '#059669', 'fast', 4), +('周**', '自动化仪表工程师', '协鑫集团有限公司', '8k', '#d97706', 'fast', 5), +('吴**', '电气自动化工程师', '新誉集团有限公司', '9k', '#0891b2', 'fast', 6), +('郑**', 'PLC工程师', '中天科技集团有限公司', '11k', '#4f46e5', 'fast', 7), +('王**', '自动化产线维护员', '海澜集团有限公司', '8k', '#be185d', 'fast', 8), +('冯**', '机电工程师', '红豆集团有限公司', '10k', '#ca8a04', 'fast', 9), +('陈**', '自动化控制技术员', '江苏国泰国际集团股份有限公司', '12k', '#16a34a', 'fast', 10), +('褚**', '智能制造工程师', '瑞声科技控股有限公司', '9k', '#ea580c', 'fast', 11), +('卫**', '机器人维护工程师', '天合光能股份有限公司', '11k', '#0284c7', 'fast', 12); + +-- slow轨道 (12条) +INSERT INTO success_stories (name, job, company, salary, avatar_color, track_type, display_order) VALUES +('蒋**', '自动化测试工程师', '信达生物制药(苏州)有限公司', '12k', '#9333ea', 'slow', 1), +('沈**', '电气设计师', '同程网络科技股份有限公司', '10k', '#dc2626', 'slow', 2), +('韩**', '设备自动化工程师', '江苏新潮科技集团有限公司', '9k', '#4b5563', 'slow', 3), +('杨**', '工控网络工程师', '远东控股集团有限公司', '11k', '#0d9488', 'slow', 4), +('朱**', '自动化系统集成工程师', '江苏中南建设集团股份有限公司', '12k', '#2563eb', 'slow', 5), +('秦**', '现场应用工程师', '苏宁易购集团股份有限公司', '8k', '#db2777', 'slow', 6), +('尤**', '自动化运维工程师', '南京钢铁集团有限公司', '9k', '#7c3aed', 'slow', 7), +('许**', '视觉算法工程师(助理)', '江苏扬子江船厂有限公司', '12k', '#059669', 'slow', 8), +('何**', '运动控制算法工程师', '徐工集团工程机械有限公司', '11k', '#d97706', 'slow', 9), +('吕**', '自动化项目经理助理', '中信泰富特钢集团股份有限公司', '10k', '#0891b2', 'slow', 10), +('施**', '电气调试员', '江苏悦达集团有限公司', '8k', '#4f46e5', 'slow', 11), +('张**', '自动化装配技术员', '东方润安集团有限公司', '9k', '#be185d', 'slow', 12); + +-- medium轨道 (11条) +INSERT INTO success_stories (name, job, company, salary, avatar_color, track_type, display_order) VALUES +('孔**', '自动化售后工程师', '江苏永钢集团有限公司', '8k', '#ca8a04', 'medium', 1), +('曹**', 'MES实施工程师', '江苏恒顺集团有限公司', '9k', '#16a34a', 'medium', 2), +('严**', 'SCADA开发工程师', '江苏索普(集团)有限公司', '11k', '#ea580c', 'medium', 3), +('华**', '嵌入式软件工程师', '通鼎集团有限公司', '12k', '#0284c7', 'medium', 4), +('金**', '自动化硬件工程师', '波司登股份有限公司', '10k', '#9333ea', 'medium', 5), +('魏**', '工控机维护员', '扬州泰富特种材料有限公司', '8k', '#dc2626', 'medium', 6), +('陶**', '自动化仓储运维', '江苏飞达控股集团有限公司', '9k', '#4b5563', 'medium', 7), +('姜**', 'AGV调度工程师', '大全集团有限公司', '11k', '#0d9488', 'medium', 8), +('戚**', '自动化采购专员', '弘元绿色能源股份有限公司', '8k', '#2563eb', 'medium', 9), +('谢**', '技术支持工程师', '亚邦投资控股集团有限公司', '10k', '#db2777', 'medium', 10), +('邹**', '自动化培训讲师', '江苏林洋能源股份有限公司', '12k', '#7c3aed', 'medium', 11); + +-- ========================================== +-- 恢复外键检查 +-- ========================================== +SET FOREIGN_KEY_CHECKS = 1; + +-- ========================================== +-- 数据验证 +-- ========================================== +SELECT '========== 数据验证 ==========' as message; +SELECT 'high_salary_jobs' as table_name, COUNT(*) as count FROM high_salary_jobs +UNION ALL +SELECT 'training_units', COUNT(*) FROM training_units +UNION ALL +SELECT 'success_stories', COUNT(*) FROM success_stories; + +SELECT '========== 初始化完成 ==========' as message; +SELECT '已插入 20 条高薪岗位数据' as info +UNION ALL SELECT '已插入 8 条培训单元数据' +UNION ALL SELECT '已插入 35 条成功案例数据'; diff --git a/scripts/kill-by-ids.js b/scripts/kill-by-ids.js new file mode 100644 index 0000000..535c5a2 --- /dev/null +++ b/scripts/kill-by-ids.js @@ -0,0 +1,62 @@ +/** + * 超简单的进程终止工具 + * 直接指定进程ID列表,避免复杂查询 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function killProcessIds(ids) { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + if (!ids || ids.length === 0) { + console.log('⚠️ 没有提供要终止的进程ID'); + console.log('\n使用方法:'); + console.log(' node scripts/kill-by-ids.js 123 456 789'); + console.log(' 或在代码中修改 processIds 数组\n'); + return; + } + + console.log(`🔪 准备终止 ${ids.length} 个进程: ${ids.join(', ')}\n`); + + let killed = 0; + for (const id of ids) { + try { + await connection.query(`KILL ${id}`); + console.log(`✅ 已终止进程 ${id}`); + killed++; + } catch (err) { + console.log(`⚠️ 进程 ${id} 终止失败: ${err.message}`); + } + } + + console.log(`\n✅ 成功终止 ${killed}/${ids.length} 个进程\n`); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +// 从命令行参数获取进程ID +const processIds = process.argv.slice(2).map(id => parseInt(id)).filter(id => !isNaN(id)); + +// 或者直接在这里列出要终止的进程ID(如果知道的话) +// const processIds = [123, 456, 789]; + +killProcessIds(processIds); diff --git a/scripts/kill-locked-processes.js b/scripts/kill-locked-processes.js new file mode 100644 index 0000000..e927af1 --- /dev/null +++ b/scripts/kill-locked-processes.js @@ -0,0 +1,136 @@ +/** + * 批量终止锁定的MySQL进程 + * 安全版本 - 会列出要终止的进程供确认 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); +const readline = require('readline'); + +// 创建命令行接口用于用户确认 +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +function question(query) { + return new Promise(resolve => rl.question(query, resolve)); +} + +async function killLockedProcesses() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + // 查找所有符合条件的进程 + console.log('📋 查找需要终止的进程...\n'); + + const [processes] = await connection.query(` + SELECT Id, User, Host, db, Command, Time, State, Info + FROM INFORMATION_SCHEMA.PROCESSLIST + WHERE ( + State LIKE '%waiting for handler commit%' + OR State LIKE '%Locked%' + OR (Command = 'Sleep' AND Time > 300) + ) + AND Id != CONNECTION_ID() + `); + + if (processes.length === 0) { + console.log('✅ 没有找到需要终止的进程\n'); + rl.close(); + return; + } + + // 显示找到的进程 + console.log(`找到 ${processes.length} 个需要终止的进程:\n`); + console.log('ID\t用户\t\t命令\t时间(秒)\t状态'); + console.log('─'.repeat(80)); + + processes.forEach(p => { + const user = (p.User || '').padEnd(12); + const command = (p.Command || '').padEnd(8); + const time = String(p.Time || 0).padEnd(8); + const state = (p.State || 'Sleep').substring(0, 40); + console.log(`${p.Id}\t${user}\t${command}\t${time}\t${state}`); + }); + + console.log('\n'); + + // 询问用户确认 + const answer = await question('确认要终止这些进程吗?(yes/no): '); + + if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') { + console.log('\n❌ 操作已取消\n'); + rl.close(); + return; + } + + console.log('\n🔪 开始终止进程...\n'); + + // 批量终止进程 + let successCount = 0; + let failCount = 0; + + for (const p of processes) { + try { + await connection.query(`KILL ${p.Id}`); + console.log(`✅ 已终止进程 ${p.Id} (${p.Command}, ${p.State})`); + successCount++; + } catch (err) { + console.log(`❌ 无法终止进程 ${p.Id}: ${err.message}`); + failCount++; + } + } + + console.log('\n' + '═'.repeat(80)); + console.log(`✅ 成功终止: ${successCount} 个进程`); + if (failCount > 0) { + console.log(`❌ 失败: ${failCount} 个进程`); + } + console.log('═'.repeat(80) + '\n'); + + // 验证结果 + console.log('📋 验证当前进程状态...\n'); + const [remaining] = await connection.query(` + SELECT COUNT(*) as count + FROM INFORMATION_SCHEMA.PROCESSLIST + WHERE ( + State LIKE '%waiting for handler commit%' + OR State LIKE '%Locked%' + OR (Command = 'Sleep' AND Time > 300) + ) + AND Id != CONNECTION_ID() + `); + + if (remaining[0].count === 0) { + console.log('✅ 所有锁定的进程已清除!\n'); + } else { + console.log(`⚠️ 仍有 ${remaining[0].count} 个锁定的进程\n`); + } + + rl.close(); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + console.error('\n提示:如果遇到权限问题,请联系数据库管理员\n'); + rl.close(); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +// 执行 +killLockedProcesses(); diff --git a/scripts/quick-kill-all.js b/scripts/quick-kill-all.js new file mode 100644 index 0000000..eb0b96d --- /dev/null +++ b/scripts/quick-kill-all.js @@ -0,0 +1,66 @@ +/** + * 快速批量终止锁定的MySQL进程 + * 无需确认 - 直接终止所有锁定的进程 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function quickKillAll() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + // 查找并终止所有锁定的进程 + const [processes] = await connection.query(` + SELECT Id, Command, State, Time + FROM INFORMATION_SCHEMA.PROCESSLIST + WHERE ( + State LIKE '%waiting for handler commit%' + OR State LIKE '%Locked%' + OR State LIKE '%lock%' + OR (Command = 'Sleep' AND Time > 300) + ) + AND Id != CONNECTION_ID() + `); + + if (processes.length === 0) { + console.log('✅ 没有找到锁定的进程\n'); + return; + } + + console.log(`🔪 找到 ${processes.length} 个锁定的进程,正在终止...\n`); + + let killed = 0; + for (const p of processes) { + try { + await connection.query(`KILL ${p.Id}`); + console.log(`✅ 已终止进程 ${p.Id}`); + killed++; + } catch (err) { + console.log(`⚠️ 进程 ${p.Id} 终止失败: ${err.message}`); + } + } + + console.log(`\n✅ 成功终止 ${killed}/${processes.length} 个进程\n`); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +quickKillAll(); diff --git a/scripts/run-init-sql.js b/scripts/run-init-sql.js new file mode 100644 index 0000000..de1b77a --- /dev/null +++ b/scripts/run-init-sql.js @@ -0,0 +1,46 @@ +const mysql = require('mysql2/promise'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +async function runInitSQL() { + const connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + multipleStatements: true + }); + + try { + console.log('✅ 数据库连接成功'); + + const sqlFile = path.join(__dirname, 'init-high-page-data.sql'); + const sql = fs.readFileSync(sqlFile, 'utf8'); + + console.log('📝 开始执行SQL脚本...'); + await connection.query(sql); + + console.log('✅ SQL脚本执行完成!'); + + // 验证数据 + const [jobs] = await connection.query('SELECT COUNT(*) as count FROM high_salary_jobs'); + const [units] = await connection.query('SELECT COUNT(*) as count FROM training_units'); + const [stories] = await connection.query('SELECT COUNT(*) as count FROM success_stories'); + + console.log('\n========== 数据验证 =========='); + console.log(`✅ high_salary_jobs: ${jobs[0].count} 条`); + console.log(`✅ training_units: ${units[0].count} 条`); + console.log(`✅ success_stories: ${stories[0].count} 条`); + console.log('========== 初始化完成 ==========\n'); + + } catch (error) { + console.error('❌ 执行失败:', error.message); + process.exit(1); + } finally { + await connection.end(); + } +} + +runInitSQL(); diff --git a/scripts/simple-test.js b/scripts/simple-test.js new file mode 100644 index 0000000..b8e30ba --- /dev/null +++ b/scripts/simple-test.js @@ -0,0 +1,116 @@ +/** + * 简单测试预招录确认功能 + * 避免使用复杂查询导致临时表问题 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function simpleTest() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + // 简单测试1: 查询training_confirmations表 + console.log('📋 测试1: 查询确认记录...'); + const [confirmations] = await connection.query( + 'SELECT id, user_id, training_unit_id FROM training_confirmations LIMIT 5' + ); + console.log(`✅ 成功查询到 ${confirmations.length} 条记录\n`); + + // 简单测试2: 尝试INSERT操作 + console.log('📋 测试2: 测试INSERT操作...'); + const testUserId = 99999; + const testTrainingUnitId = 99999; + + try { + // 先清理可能存在的测试数据 + await connection.query( + 'DELETE FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [testUserId, testTrainingUnitId] + ); + + // 尝试插入 + const startTime = Date.now(); + await connection.query( + 'INSERT INTO training_confirmations (user_id, training_unit_id) VALUES (?, ?)', + [testUserId, testTrainingUnitId] + ); + const endTime = Date.now(); + + console.log(`✅ INSERT操作成功 (耗时: ${endTime - startTime}ms)`); + + // 验证插入 + const [inserted] = await connection.query( + 'SELECT id FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [testUserId, testTrainingUnitId] + ); + + if (inserted.length > 0) { + console.log(`✅ 成功插入记录,ID: ${inserted[0].id}`); + } + + // 清理测试数据 + await connection.query( + 'DELETE FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [testUserId, testTrainingUnitId] + ); + console.log('✅ 已清理测试数据\n'); + + } catch (err) { + console.error('❌ INSERT操作失败:', err.message); + console.error('错误代码:', err.code); + + if (err.code === 'ER_LOCK_WAIT_TIMEOUT') { + console.error('\n⚠️ 数据库仍然存在锁定问题!'); + console.error('建议:'); + console.error('1. 联系数据库管理员检查锁定的事务'); + console.error('2. 或等待锁定的事务超时释放\n'); + process.exit(1); + } + } + + // 简单测试3: 测试UPDATE操作 + console.log('📋 测试3: 测试UPDATE操作...'); + try { + const startTime = Date.now(); + await connection.query( + 'UPDATE training_confirmations SET user_id = user_id WHERE id = 1' + ); + const endTime = Date.now(); + console.log(`✅ UPDATE操作成功 (耗时: ${endTime - startTime}ms)\n`); + } catch (err) { + console.error('❌ UPDATE操作失败:', err.message); + if (err.code === 'ER_LOCK_WAIT_TIMEOUT') { + console.error('⚠️ 数据库存在锁定问题\n'); + } + } + + console.log('========================================'); + console.log('✅ 基本功能测试完成!'); + console.log('========================================\n'); + console.log('💡 数据库连接和基本操作正常'); + console.log('📍 现在可以在浏览器中测试预招录确认按钮'); + console.log('🌐 访问: http://localhost:8080/high.html\n'); + + } catch (error) { + console.error('❌ 测试失败:', error.message); + console.error('错误代码:', error.code); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +simpleTest(); diff --git a/scripts/test-permissions.js b/scripts/test-permissions.js new file mode 100644 index 0000000..5e3f670 --- /dev/null +++ b/scripts/test-permissions.js @@ -0,0 +1,92 @@ +/** + * 测试数据库权限 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +const dbConfig = { + host: process.env.DB_HOST, + port: process.env.DB_PORT, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE || 'ddcz_platform' +}; + +async function testPermissions() { + let connection; + + try { + console.log('🔌 正在连接到MySQL数据库...'); + console.log(` 主机: ${dbConfig.host}:${dbConfig.port}`); + console.log(` 用户: ${dbConfig.user}`); + console.log(` 数据库: ${dbConfig.database}\n`); + + connection = await mysql.createConnection(dbConfig); + console.log('✅ 数据库连接成功!\n'); + + // 测试1: 查看当前用户权限 + console.log('📋 测试1: 查看用户权限...'); + const [grants] = await connection.query('SHOW GRANTS'); + console.log('✅ 当前用户权限:'); + grants.forEach(grant => { + console.log(` ${Object.values(grant)[0]}`); + }); + console.log(''); + + // 测试2: 创建测试表 + console.log('📋 测试2: 测试创建表权限...'); + await connection.query(` + CREATE TABLE IF NOT EXISTS test_permissions ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + `); + console.log('✅ 创建表权限正常\n'); + + // 测试3: 插入数据 + console.log('📋 测试3: 测试插入数据权限...'); + await connection.query(` + INSERT INTO test_permissions (name) VALUES ('test_data') + `); + console.log('✅ 插入数据权限正常\n'); + + // 测试4: 修改表结构 + console.log('📋 测试4: 测试修改表结构权限...'); + await connection.query(` + ALTER TABLE test_permissions ADD COLUMN description TEXT + `); + console.log('✅ 修改表结构权限正常\n'); + + // 测试5: 删除表 + console.log('📋 测试5: 测试删除表权限...'); + await connection.query(` + DROP TABLE IF EXISTS test_permissions + `); + console.log('✅ 删除表权限正常\n'); + + console.log('🎉 所有权限测试通过!'); + console.log('✅ 您的账号具有完整的数据库操作权限'); + console.log('📌 可以继续运行 npm run init-db 创建所有表\n'); + + } catch (error) { + console.error('\n❌ 权限测试失败:'); + console.error(error.message); + + if (error.code === 'ER_DBACCESS_DENIED_ERROR') { + console.error('\n💡 没有访问数据库的权限'); + } else if (error.code === 'ER_TABLEACCESS_DENIED_ERROR') { + console.error('\n💡 没有操作表的权限'); + } + + process.exit(1); + } finally { + if (connection) { + await connection.end(); + console.log('🔌 数据库连接已关闭'); + } + } +} + +testPermissions(); diff --git a/scripts/test-training-confirmation.js b/scripts/test-training-confirmation.js new file mode 100644 index 0000000..2ff7801 --- /dev/null +++ b/scripts/test-training-confirmation.js @@ -0,0 +1,125 @@ +/** + * 测试预招录确认功能是否正常 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function testConfirmation() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + // 测试1: 检查是否还有锁定的进程 + console.log('📋 测试1: 检查锁定的进程...'); + const [lockedProcesses] = await connection.query( + "SELECT Id, State, Info FROM INFORMATION_SCHEMA.PROCESSLIST WHERE State LIKE '%waiting for handler commit%' OR State LIKE '%Locked%'" + ); + + if (lockedProcesses.length > 0) { + console.log(`⚠️ 发现 ${lockedProcesses.length} 个锁定的进程:`); + lockedProcesses.forEach(p => { + console.log(` - 进程 ${p.Id}: ${p.State}`); + }); + } else { + console.log('✅ 没有发现锁定的进程\n'); + } + + // 测试2: 查看当前所有进程状态 + console.log('📋 测试2: 当前活动进程...'); + const [allProcesses] = await connection.query('SHOW FULL PROCESSLIST'); + console.log(`当前活动进程数: ${allProcesses.length}`); + + const sleepCount = allProcesses.filter(p => p.Command === 'Sleep').length; + const queryCount = allProcesses.filter(p => p.Command === 'Query').length; + console.log(` - Sleep状态: ${sleepCount}`); + console.log(` - Query状态: ${queryCount}\n`); + + // 测试3: 检查training_confirmations表是否可以正常查询 + console.log('📋 测试3: 查询training_confirmations表...'); + const [confirmations] = await connection.query( + 'SELECT COUNT(*) as count FROM training_confirmations' + ); + console.log(`✅ 当前共有 ${confirmations[0].count} 条确认记录\n`); + + // 测试4: 测试是否可以执行INSERT操作(使用一个不存在的user_id和training_unit_id) + console.log('📋 测试4: 测试INSERT操作...'); + const testUserId = 99999; + const testTrainingUnitId = 99999; + + try { + // 先检查是否存在 + const [existing] = await connection.query( + 'SELECT id FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [testUserId, testTrainingUnitId] + ); + + if (existing.length === 0) { + // 尝试插入 + await connection.query( + 'INSERT INTO training_confirmations (user_id, training_unit_id) VALUES (?, ?)', + [testUserId, testTrainingUnitId] + ); + console.log('✅ INSERT操作成功'); + + // 清理测试数据 + await connection.query( + 'DELETE FROM training_confirmations WHERE user_id = ? AND training_unit_id = ?', + [testUserId, testTrainingUnitId] + ); + console.log('✅ 已清理测试数据\n'); + } else { + console.log('⚠️ 测试记录已存在,跳过INSERT测试\n'); + } + } catch (err) { + console.error('❌ INSERT操作失败:', err.message); + if (err.code === 'ER_LOCK_WAIT_TIMEOUT') { + console.error('⚠️ 数据库仍然存在锁定问题!\n'); + } + } + + // 测试5: 检查是否有重复记录 + console.log('📋 测试5: 检查重复记录...'); + const [duplicates] = await connection.query(` + SELECT user_id, training_unit_id, COUNT(*) as count + FROM training_confirmations + GROUP BY user_id, training_unit_id + HAVING count > 1 + `); + + if (duplicates.length > 0) { + console.log(`⚠️ 发现 ${duplicates.length} 组重复记录:`); + duplicates.forEach(dup => { + console.log(` - user_id: ${dup.user_id}, training_unit_id: ${dup.training_unit_id}, 数量: ${dup.count}`); + }); + } else { + console.log('✅ 没有发现重复记录\n'); + } + + console.log('========================================'); + console.log('✅ 所有测试完成!'); + console.log('========================================\n'); + console.log('💡 现在可以在浏览器中测试预招录确认按钮了'); + console.log('📍 访问: http://localhost:8080/high.html'); + + } catch (error) { + console.error('❌ 测试失败:', error.message); + console.error('错误详情:', error); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +testConfirmation(); diff --git a/scripts/unlock-all-tables.js b/scripts/unlock-all-tables.js new file mode 100644 index 0000000..f9de9eb --- /dev/null +++ b/scripts/unlock-all-tables.js @@ -0,0 +1,44 @@ +/** + * 解锁所有表 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function unlockAllTables() { + let connection; + + try { + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库\n'); + + // 执行UNLOCK TABLES + console.log('🔓 正在解锁所有表...'); + await connection.query('UNLOCK TABLES'); + console.log('✅ 所有表已解锁\n'); + + // 提交任何待处理的事务 + console.log('📝 提交待处理的事务...'); + await connection.query('COMMIT'); + console.log('✅ 事务已提交\n'); + + console.log('✅ 数据库解锁完成!\n'); + + } catch (error) { + console.error('❌ 操作失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +unlockAllTables(); diff --git a/scripts/unlock-tables.js b/scripts/unlock-tables.js new file mode 100644 index 0000000..a840481 --- /dev/null +++ b/scripts/unlock-tables.js @@ -0,0 +1,68 @@ +/** + * 解锁数据库表 + * 终止所有锁定的连接并重置事务 + */ + +require('dotenv').config(); +const mysql = require('mysql2/promise'); + +async function unlockTables() { + let connection; + + try { + // 创建数据库连接 + connection = await mysql.createConnection({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE + }); + + console.log('✅ 已连接到数据库'); + + // 1. 查看当前进程 + console.log('\n📋 查看当前锁定的进程...'); + const [processes] = await connection.query('SHOW FULL PROCESSLIST'); + + console.log('当前活动进程数:', processes.length); + processes.forEach(p => { + console.log(` - ID: ${p.Id}, User: ${p.User}, State: ${p.State}, Info: ${p.Info}`); + }); + + // 2. 杀掉Sleep状态的连接(这些可能持有未提交的事务) + console.log('\n🔪 终止Sleep状态的连接...'); + let killedCount = 0; + for (const p of processes) { + if (p.Command === 'Sleep' && p.Id !== connection.threadId) { + try { + await connection.query(`KILL ${p.Id}`); + console.log(` ✅ 已终止进程 ${p.Id}`); + killedCount++; + } catch (err) { + console.log(` ⚠️ 无法终止进程 ${p.Id}:`, err.message); + } + } + } + + console.log(`\n终止了 ${killedCount} 个连接`); + + // 3. 解锁所有表 + console.log('\n🔓 解锁所有表...'); + await connection.query('UNLOCK TABLES'); + console.log('✅ 表已解锁'); + + console.log('\n✅ 数据库解锁完成!'); + + } catch (error) { + console.error('❌ 解锁失败:', error.message); + process.exit(1); + } finally { + if (connection) { + await connection.end(); + } + } +} + +// 执行 +unlockTables(); diff --git a/server.js b/server.js index bc21c5d..d358e14 100644 --- a/server.js +++ b/server.js @@ -1,60 +1,137 @@ -const http = require('http'); -const fs = require('fs'); +/** + * 多多畅职大专生就业服务平台 - 后端服务器 + * 基于Express框架 + */ + +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); const path = require('path'); +const os = require('os'); -const PORT = 8080; +// 导入路由 +const authRoutes = require('./api/routes/auth'); +const applicationRoutes = require('./api/routes/applications'); +const highSalaryJobsRoutes = require('./api/routes/highSalaryJobs'); +const trainingUnitsRoutes = require('./api/routes/trainingUnits'); +const successStoriesRoutes = require('./api/routes/successStories'); +const transitionProgressRoutes = require('./api/routes/transitionProgress'); -// MIME类型映射 -const mimeTypes = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.json': 'application/json', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.ttf': 'font/ttf', - '.csv': 'text/csv' -}; +// 创建Express应用 +const app = express(); +const PORT = process.env.PORT || 8080; -const server = http.createServer((req, res) => { - // 解析请求的URL并解码(支持中文文件名) - let filePath = '.' + decodeURIComponent(req.url); - if (filePath === './') { - filePath = './index.html'; - } +// ======================================== +// 中间件配置 +// ======================================== - // 获取文件扩展名 - const extname = String(path.extname(filePath)).toLowerCase(); - const contentType = mimeTypes[extname] || 'application/octet-stream'; +// CORS配置 +app.use(cors({ + origin: '*', // 允许所有来源(生产环境应该限制) + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: ['Content-Type', 'Authorization'] +})); - // 读取文件 - fs.readFile(filePath, (error, content) => { - if (error) { - if (error.code === 'ENOENT') { - // 文件不存在 - res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end('

404 - 文件未找到

', 'utf-8'); - } else { - // 服务器错误 - res.writeHead(500); - res.end('服务器错误: ' + error.code, 'utf-8'); - } - } else { - // 成功返回文件 - res.writeHead(200, { 'Content-Type': contentType }); - res.end(content, 'utf-8'); - } +// 解析JSON请求体 +app.use(express.json({ limit: '10mb' })); + +// 解析URL编码的请求体 +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// 请求日志 +app.use((req, res, next) => { + const timestamp = new Date().toISOString(); + console.log(`[${timestamp}] ${req.method} ${req.url}`); + next(); +}); + +// ======================================== +// API路由 +// ======================================== + +// 健康检查 +app.get('/api/health', (req, res) => { + res.json({ + success: true, + message: '服务器运行正常', + timestamp: new Date().toISOString() }); }); +// 用户认证相关路由 +app.use('/api/auth', authRoutes); + +// 投递记录相关路由 +app.use('/api/applications', applicationRoutes); + +// 高薪岗位相关路由 +app.use('/api/high-salary-jobs', highSalaryJobsRoutes); + +// 培训单元相关路由 +app.use('/api/training-units', trainingUnitsRoutes); + +// 成功案例相关路由 +app.use('/api/success-stories', successStoriesRoutes); + +// 面试进度相关路由 +app.use('/api/transition-progress', transitionProgressRoutes); + +// API 404处理 +app.use('/api/*', (req, res) => { + res.status(404).json({ + success: false, + message: 'API接口不存在' + }); +}); + +// ======================================== +// 静态文件服务 +// ======================================== + +// 静态文件支持(保留原有的静态文件服务功能) +app.use(express.static('.', { + index: 'index.html', + setHeaders: (res, filePath) => { + // 设置缓存策略 + if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache'); + } else if (filePath.match(/\.(css|js|jpg|png|gif|svg|woff|woff2|ttf)$/)) { + res.setHeader('Cache-Control', 'public, max-age=31536000'); + } + } +})); + +// 处理前端路由(SPA fallback) +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + +// ======================================== +// 错误处理 +// ======================================== + +// 404错误处理 +app.use((req, res) => { + res.status(404).json({ + success: false, + message: '请求的资源不存在' + }); +}); + +// 全局错误处理 +app.use((err, req, res, next) => { + console.error('服务器错误:', err); + res.status(500).json({ + success: false, + message: process.env.NODE_ENV === 'development' ? err.message : '服务器内部错误' + }); +}); + +// ======================================== +// 启动服务器 +// ======================================== + // 获取本机IP地址 -const os = require('os'); function getLocalIP() { const interfaces = os.networkInterfaces(); for (const name of Object.keys(interfaces)) { @@ -69,15 +146,26 @@ function getLocalIP() { const localIP = getLocalIP(); -server.listen(PORT, '0.0.0.0', () => { +const server = app.listen(PORT, '0.0.0.0', () => { console.log(''); console.log('========================================'); - console.log('🚀 本地服务器已启动(局域网模式)!'); + console.log('🚀 多多畅职就业服务平台已启动!'); console.log('========================================'); console.log(''); console.log(`📍 本机访问: http://localhost:${PORT}`); console.log(`📍 局域网访问: http://${localIP}:${PORT}`); console.log(''); + console.log('🔌 API服务:'); + console.log(` - 健康检查: http://localhost:${PORT}/api/health`); + console.log(` - 用户注册: POST /api/auth/register`); + console.log(` - 用户登录: POST /api/auth/login`); + console.log(` - 获取用户: GET /api/auth/me`); + console.log(` - 投递记录: GET /api/applications`); + console.log(` - 高薪岗位: GET /api/high-salary-jobs`); + console.log(` - 培训单元: GET /api/training-units`); + console.log(` - 成功案例: GET /api/success-stories`); + console.log(` - 面试进度: GET /api/transition-progress`); + console.log(''); console.log('🌐 同一局域网内的其他设备可以通过局域网地址访问'); console.log(''); console.log('💡 提示: 按 Ctrl+C 停止服务器'); @@ -88,6 +176,16 @@ server.listen(PORT, '0.0.0.0', () => { // 优雅关闭 process.on('SIGINT', () => { - console.log('\n\n服务器已关闭'); - process.exit(0); + console.log('\n\n🔌 正在关闭服务器...'); + server.close(() => { + console.log('✅ 服务器已关闭'); + process.exit(0); + }); }); + +// 处理未捕获的Promise拒绝 +process.on('unhandledRejection', (reason, promise) => { + console.error('未处理的Promise拒绝:', reason); +}); + +module.exports = app; diff --git a/专业大类.json b/专业大类.json new file mode 100644 index 0000000..79de153 --- /dev/null +++ b/专业大类.json @@ -0,0 +1,78 @@ +[ + { + "大类名称": "农林牧渔大类", + "大类代码": "410101" + }, + { + "大类名称": "资源环境与安全大类", + "大类代码": "420101" + }, + { + "大类名称": "能源动力与材料大类", + "大类代码": "430101" + }, + { + "大类名称": "土木建筑大类", + "大类代码": "440101" + }, + { + "大类名称": "水利大类", + "大类代码": "450101" + }, + { + "大类名称": "装备制造大类", + "大类代码": "460101" + }, + { + "大类名称": "生物与化工大类", + "大类代码": "470101" + }, + { + "大类名称": "轻工纺织大类", + "大类代码": "480101" + }, + { + "大类名称": "食品药品与粮食大类", + "大类代码": "490101" + }, + { + "大类名称": "交通运输大类", + "大类代码": "500101" + }, + { + "大类名称": "电子与信息大类", + "大类代码": "510101" + }, + { + "大类名称": "医药卫生大类", + "大类代码": "520101" + }, + { + "大类名称": "财经商贸大类", + "大类代码": "530101" + }, + { + "大类名称": "旅游大类", + "大类代码": "540101" + }, + { + "大类名称": "文化艺术大类", + "大类代码": "550101" + }, + { + "大类名称": "新闻传播大类", + "大类代码": "560101" + }, + { + "大类名称": "教育与体育大类", + "大类代码": "570101" + }, + { + "大类名称": "公安与司法大类", + "大类代码": "580101" + }, + { + "大类名称": "公共管理与服务大类", + "大类代码": "590101" + } +] \ No newline at end of file diff --git a/启动服务器.bat b/启动服务器.bat deleted file mode 100644 index 0854253..0000000 --- a/启动服务器.bat +++ /dev/null @@ -1,51 +0,0 @@ -@echo off -chcp 65001 >nul -title 多多畅职大专生就业服务平台 - 局域网服务器 - -echo. -echo ======================================== -echo 多多畅职大专生就业服务平台 -echo 局域网服务器启动脚本 (Windows) -echo ======================================== -echo. - -:: 检查 Node.js 是否安装 -where node >nul 2>nul -if %errorlevel% neq 0 ( - echo [错误] 未检测到 Node.js! - echo. - echo 请先安装 Node.js: - echo 下载地址: https://nodejs.org/ - echo. - pause - exit /b 1 -) - -:: 显示 Node.js 版本 -echo [信息] 检测到 Node.js 版本: -node --version -echo. - -:: 检查 server.js 是否存在 -if not exist "server.js" ( - echo [错误] 找不到 server.js 文件! - echo 请确保在项目根目录运行此脚本。 - echo. - pause - exit /b 1 -) - -:: 启动服务器 -echo [启动] 正在启动局域网服务器... -echo. -echo ======================================== -echo 提示: 按 Ctrl+C 可停止服务器 -echo ======================================== -echo. - -node server.js - -:: 如果服务器意外退出 -echo. -echo [提示] 服务器已停止运行 -pause diff --git a/启动服务器.sh b/启动服务器.sh deleted file mode 100755 index 311f2d8..0000000 --- a/启动服务器.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -# 设置脚本在遇到错误时不退出(允许自定义错误处理) -# set -e - -# 颜色定义 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -# 清屏 -clear - -echo "" -echo "${CYAN}========================================" -echo " 多多畅职大专生就业服务平台" -echo " 局域网服务器启动脚本 (macOS/Linux)" -echo "========================================${NC}" -echo "" - -# 检查 Node.js 是否安装 -if ! command -v node &> /dev/null; then - echo "${RED}[错误] 未检测到 Node.js!${NC}" - echo "" - echo "请先安装 Node.js:" - echo "下载地址: ${BLUE}https://nodejs.org/${NC}" - echo "" - echo "或使用 Homebrew 安装:" - echo "${YELLOW}brew install node${NC}" - echo "" - exit 1 -fi - -# 显示 Node.js 版本 -echo "${GREEN}[信息] 检测到 Node.js 版本:${NC}" -node --version -echo "" - -# 检查 server.js 是否存在 -if [ ! -f "server.js" ]; then - echo "${RED}[错误] 找不到 server.js 文件!${NC}" - echo "请确保在项目根目录运行此脚本。" - echo "" - echo "当前目录: ${YELLOW}$(pwd)${NC}" - echo "" - exit 1 -fi - -# 获取本机局域网IP -LOCAL_IP=$(ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -n 1) - -echo "${GREEN}[启动] 正在启动局域网服务器...${NC}" -echo "" -echo "${CYAN}========================================" -echo "提示: 按 Ctrl+C 可停止服务器" -echo "" -echo "预计访问地址:" -echo "📍 本机: ${YELLOW}http://localhost:8080${NC}" -echo "📍 局域网: ${YELLOW}http://${LOCAL_IP}:8080${NC}" -echo "========================================${NC}" -echo "" - -# 启动服务器 -node server.js - -# 如果服务器意外退出 -echo "" -echo "${YELLOW}[提示] 服务器已停止运行${NC}" -echo "" diff --git a/快捷部署说明.md b/快捷部署说明.md deleted file mode 100644 index 5fb0600..0000000 --- a/快捷部署说明.md +++ /dev/null @@ -1,144 +0,0 @@ -# 🚀 快捷部署指南 - -本项目提供了 Windows 和 macOS 的一键启动脚本,方便快速部署到局域网。 - ---- - -## 📦 前置要求 - -### 必须安装 Node.js - -- **macOS**: - - 官方下载: https://nodejs.org/ - - Homebrew 安装: `brew install node` - -- **Windows**: - - 官方下载: https://nodejs.org/ - - 建议下载 LTS(长期支持)版本 - ---- - -## 🍎 macOS / Linux 使用方法 - -### 方法一:双击运行(推荐) -1. 找到项目文件夹中的 `启动服务器.sh` -2. **双击**运行(系统会自动使用终端打开) -3. 如果提示权限问题,请使用方法二 - -### 方法二:命令行运行 -```bash -# 1. 打开终端(Terminal) -# 2. 进入项目目录 -cd /Users/apple/Documents/cursor/三合一页面 - -# 3. 运行脚本 -./启动服务器.sh -``` - -### 方法三:直接拖拽 -1. 打开终端(Terminal) -2. 拖拽 `启动服务器.sh` 到终端窗口 -3. 按回车 - ---- - -## 🪟 Windows 使用方法 - -### 方法一:双击运行(推荐) -1. 找到项目文件夹中的 `启动服务器.bat` -2. **双击**运行 -3. 会自动打开命令提示符窗口 - -### 方法二:右键管理员运行(可选) -1. 右键点击 `启动服务器.bat` -2. 选择「以管理员身份运行」 -3. 点击「是」允许运行 - ---- - -## 📱 如何访问 - -脚本启动后,会显示访问地址: - -``` -📍 本机访问: http://localhost:8080 -📍 局域网访问: http://192.168.2.9:8080 -``` - -### 本机访问 -- 在启动服务器的电脑上打开浏览器 -- 输入: `http://localhost:8080` - -### 手机/平板访问 -1. 确保设备连接到**同一个 WiFi** -2. 打开浏览器 -3. 输入局域网地址(如 `http://192.168.2.9:8080`) - ---- - -## ⏹️ 如何停止服务器 - -- **macOS / Linux**: 按 `Ctrl + C` -- **Windows**: 按 `Ctrl + C` 或直接关闭窗口 - ---- - -## ❓ 常见问题 - -### 1. 提示「找不到 Node.js」 -**原因**: 未安装 Node.js -**解决**: 访问 https://nodejs.org/ 下载安装 - -### 2. macOS 提示「权限被拒绝」 -**解决方法**: -```bash -chmod +x 启动服务器.sh -``` - -### 3. 手机无法访问局域网地址 -**检查清单**: -- ✅ 手机和电脑是否连接到同一个 WiFi -- ✅ 服务器是否正在运行 -- ✅ 电脑防火墙是否阻止了 8080 端口 -- ✅ IP 地址是否输入正确 - -**macOS 防火墙设置**: -1. 系统偏好设置 → 安全性与隐私 → 防火墙 -2. 点击「防火墙选项」 -3. 确保允许 Node.js 的网络连接 - -**Windows 防火墙设置**: -1. 控制面板 → Windows Defender 防火墙 -2. 高级设置 → 入站规则 -3. 新建规则,允许端口 8080 - -### 4. 端口 8080 被占用 -**解决方法**: 修改 `server.js` 中的端口号 -```javascript -const PORT = 8080; // 改成其他端口,如 3000 -``` - ---- - -## 🔧 高级配置 - -如需修改端口或其他配置,请编辑 `server.js` 文件: - -```javascript -const PORT = 8080; // 修改端口号 -``` - -修改后重新运行启动脚本即可。 - ---- - -## 📞 技术支持 - -如遇到其他问题,请检查: -1. Node.js 版本: `node --version` (建议 v14 以上) -2. 项目文件是否完整 -3. 防火墙设置 - ---- - -**祝使用愉快!** 🎉