const Framework = require("../../framework/node-core-framework.js"); module.exports = { /** * @swagger * /api/user/login: * post: * summary: 用户登录 * description: 通过邮箱和密码登录,返回token、device_id和用户信息 * tags: [前端-用户管理] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - email * - password * properties: * email: * type: string * description: 邮箱(登录名) * example: 'user@example.com' * password: * type: string * description: 密码 * example: 'password123' * device_id: * type: string * description: 设备ID(客户端生成,可选,如果提供则使用提供的,否则使用数据库中的) * example: 'device_1234567890abcdef' * responses: * 200: * description: 登录成功 * content: * application/json: * schema: * type: object * properties: * code: * type: integer * description: 状态码,0表示成功 * example: 0 * message: * type: string * description: 响应消息 * example: 'success' * data: * type: object * properties: * token: * type: string * description: 认证token * example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' * device_id: * type: string * description: 设备ID * example: 'device_123456' * user: * type: object * description: 用户信息 * 400: * description: 参数错误或用户不存在 * content: * application/json: * schema: * type: object * properties: * code: * type: integer * example: 400 * message: * type: string * example: '用户不存在或密码错误' */ "POST /user/login": async (ctx) => { const { login_name:email, password, device_id: client_device_id } = ctx.getBody(); const dayjs = require('dayjs'); const { verifyPassword, validateDeviceId, maskEmail } = require('../utils/crypto_utils'); // 参数验证 if (!email || !password) { return ctx.fail('邮箱和密码不能为空'); } // 统一邮箱地址为小写 const email_normalized = email.toLowerCase().trim(); // 验证邮箱格式 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email_normalized)) { return ctx.fail('邮箱格式不正确'); } // 验证密码长度 if (password.length < 6 || password.length > 50) { return ctx.fail('密码长度不正确'); } const { pla_account } = await Framework.getModels(); try { // 根据邮箱查找用户(使用统一的小写邮箱) const user = await pla_account.findOne({ where: { login_name: email_normalized } }); if (!user) { // 防止用户枚举攻击:统一返回相同的错误信息 return ctx.fail('邮箱或密码错误'); } // 验证密码(支持新旧格式) let isPasswordValid = false; if (user.pwd && user.pwd.includes('$')) { // 新格式:加密密码 isPasswordValid = await verifyPassword(password, user.pwd); } else { // 旧格式:明文密码(临时兼容,建议尽快迁移) isPasswordValid = (user.pwd === password); // 如果验证通过,自动升级为加密密码 if (isPasswordValid) { const { hashPassword } = require('../utils/crypto_utils'); const hashedPassword = await hashPassword(password); await pla_account.update( { pwd: hashedPassword }, { where: { id: user.id } } ); console.log(`[安全升级] 用户 ${maskEmail(email_normalized)} 的密码已自动加密`); } } if (!isPasswordValid) { // 记录失败尝试(用于后续添加防暴力破解功能) console.warn(`[登录失败] 邮箱: ${maskEmail(email_normalized)}, 时间: ${new Date().toISOString()}`); return ctx.fail('邮箱或密码错误'); } // 检查账号是否启用 if (!user.is_enabled) { return ctx.fail('账号已被禁用,请联系管理员'); } // 检查授权状态(仅记录,不拒绝登录) const userData = user.toJSON(); const authDate = userData.authorization_date; const authDays = userData.authorization_days || 0; // 使用工具函数计算剩余天数 const { calculateRemainingDays } = require('../utils/account_utils'); const remaining_days = calculateRemainingDays(authDate, authDays); // 处理设备ID let device_id = client_device_id; // 验证客户端提供的 device_id 格式 if (client_device_id) { // if (!validateDeviceId(client_device_id)) { // return ctx.fail('设备ID格式不正确,请重新登录'); // } // 如果与数据库不同,需要额外验证(防止设备ID劫持) if (user.device_id && client_device_id !== user.device_id) { // 记录设备更换 console.warn(`[设备更换] 用户 ${maskEmail(email_normalized)} 更换设备: ${user.device_id} -> ${client_device_id}`); // TODO: 这里可以添加更多安全检查,如: // - 发送验证码确认 // - 记录到安全日志 // - 限制更换频率 } // 更新设备ID if (client_device_id !== user.device_id) { await pla_account.update( { device_id: client_device_id }, { where: { id: user.id } } ); } } else { // 使用数据库中的设备ID device_id = user.device_id; } // 如果仍然没有设备ID,返回错误 if (!device_id) { return ctx.fail('设备ID不能为空,请重新登录'); } // 创建token const token = Framework.getServices().tokenService.create({ sn_code: user.sn_code, device_id: device_id, id: user.id }); // 构建安全的用户信息响应(白名单模式) const safeUserInfo = { id: user.id, sn_code: user.sn_code, login_name: maskEmail(user.login_name), // 脱敏处理 is_enabled: user.is_enabled, authorization_date: user.authorization_date, authorization_days: user.authorization_days, platform_type: user.platform_type, remaining_days: remaining_days, auto_deliver: user.auto_deliver, deliver_config: user.deliver_config, created_at: user.create_time, updated_at: user.last_modify_time }; // 记录成功登录 console.log(`[登录成功] 用户 ${maskEmail(email_normalized)}, 剩余天数: ${remaining_days}`); return ctx.success({ token, device_id, user: safeUserInfo }); } catch (error) { // 记录详细错误但不暴露给客户端 console.error('[登录异常]', { error: error.message, stack: error.stack, email: maskEmail(email_normalized) }); return ctx.fail('登录失败,请稍后重试'); } }, /** * @swagger * /api/user/delivery-config/get: * post: * summary: 获取投递配置 * description: 根据设备SN码获取用户的投递配置,返回 pla_account 表中的 deliver_config 对象 * tags: [前端-用户管理] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - sn_code * properties: * sn_code: * type: string * description: 设备SN码 * responses: * 200: * description: 获取成功 * content: * application/json: * schema: * type: object * properties: * code: * type: integer * example: 0 * message: * type: string * example: 'success' * data: * type: object * properties: * deliver_config: * type: object * description: 自动投递配置对象,如果不存在则返回 null * nullable: true */ 'POST /user/delivery-config/get': async (ctx) => { try { const body = ctx.getBody(); const { sn_code } = body; if (!sn_code) { return ctx.fail('请提供设备SN码'); } const { pla_account } = await Framework.getModels(); // 根据 sn_code 查找账号 const user = await pla_account.findOne({ where: { sn_code } }); if (!user) { return ctx.fail('用户不存在'); } // 从 pla_account 表的 deliver_config 字段获取配置 const deliver_config = user.deliver_config || null; return ctx.success({ deliver_config }); } catch (error) { console.error('[获取投递配置失败]', { error: error.message, timestamp: new Date().toISOString() }); return ctx.fail('获取投递配置失败'); } }, /** * @swagger * /api/user/delivery-config/save: * post: * summary: 保存投递配置 * description: 根据设备SN码保存用户的投递配置到 pla_account 表的 deliver_config 字段 * tags: [前端-用户管理] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - sn_code * - deliver_config * properties: * sn_code: * type: string * description: 设备SN码 * deliver_config: * type: object * description: 自动投递配置对象 * properties: * auto_delivery: * type: boolean * description: 是否启用自动投递 * interval: * type: integer * description: 投递间隔(分钟) * min_salary: * type: integer * description: 最低薪资 * max_salary: * type: integer * description: 最高薪资 * scroll_pages: * type: integer * description: 滚动页数 * max_per_batch: * type: integer * description: 每批最多投递数 * filter_keywords: * type: string * description: 过滤关键词 * exclude_keywords: * type: string * description: 排除关键词 * start_time: * type: string * description: 开始时间(格式:HH:mm) * end_time: * type: string * description: 结束时间(格式:HH:mm) * workdays_only: * type: boolean * description: 仅工作日 * responses: * 200: * description: 保存成功 */ 'POST /user/delivery-config/save': async (ctx) => { try { const body = ctx.getBody(); const { sn_code, deliver_config } = body; if (!sn_code) { return ctx.fail('请提供设备SN码'); } if (!deliver_config) { return ctx.fail('请提供 deliver_config 配置对象'); } // 验证 deliver_config 的基本结构 if (typeof deliver_config !== 'object') { return ctx.fail('deliver_config 必须是对象'); } const { pla_account } = await Framework.getModels(); // 根据 sn_code 查找账号 const user = await pla_account.findOne({ where: { sn_code } }); if (!user) { return ctx.fail('用户不存在'); } // 更新 pla_account 表的 deliver_config 和 auto_deliver 字段 // 查出来合并原来的字段,如何没传就不覆盖 const original_deliver_config = user.deliver_config || {}; const new_deliver_config = { ...original_deliver_config, ...deliver_config }; await pla_account.update( { deliver_config: new_deliver_config, auto_deliver: deliver_config.auto_delivery ? 1 : 0 }, { where: { id: user.id } } ); console.log('[保存投递配置成功]', { sn_code, auto_delivery: deliver_config.auto_delivery, timestamp: new Date().toISOString() }); return ctx.success({ message: '配置保存成功' }); } catch (error) { console.error('[保存投递配置失败]', { error: error.message, timestamp: new Date().toISOString() }); return ctx.fail('保存投递配置失败'); } } }