427 lines
14 KiB
JavaScript
427 lines
14 KiB
JavaScript
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:
|
||
* - phone
|
||
* - password
|
||
* properties:
|
||
* phone:
|
||
* type: string
|
||
* description: 手机号(登录名)
|
||
* example: '13800138000'
|
||
* 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 { phone, password, device_id: client_device_id } = ctx.getBody();
|
||
const dayjs = require('dayjs');
|
||
const { verifyPassword, validateDeviceId, maskPhone } = require('../utils/crypto_utils');
|
||
const { isAuthorizationValid } = require('../utils/account_utils');
|
||
|
||
// 参数验证
|
||
if (!phone || !password) {
|
||
return ctx.fail('手机号和密码不能为空');
|
||
}
|
||
|
||
// 验证手机号格式
|
||
const phonePattern = /^1[3-9]\d{9}$/;
|
||
if (!phonePattern.test(phone)) {
|
||
return ctx.fail('手机号格式不正确');
|
||
}
|
||
|
||
// 验证密码长度
|
||
if (password.length < 6 || password.length > 50) {
|
||
return ctx.fail('密码长度不正确');
|
||
}
|
||
|
||
const { pla_account } = await Framework.getModels();
|
||
|
||
try {
|
||
// 根据手机号查找用户(使用参数化查询防止SQL注入)
|
||
const user = await pla_account.findOne({
|
||
where: {
|
||
login_name: phone
|
||
}
|
||
});
|
||
|
||
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(`[安全升级] 用户 ${maskPhone(phone)} 的密码已自动加密`);
|
||
}
|
||
}
|
||
|
||
if (!isPasswordValid) {
|
||
// 记录失败尝试(用于后续添加防暴力破解功能)
|
||
console.warn(`[登录失败] 手机号: ${maskPhone(phone)}, 时间: ${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;
|
||
|
||
// 验证授权有效性
|
||
if (!isAuthorizationValid(authDate, authDays)) {
|
||
console.warn(`[授权过期] 用户 ${maskPhone(phone)} 尝试登录但授权已过期`);
|
||
return ctx.fail('账号授权已过期,请续费后使用');
|
||
}
|
||
|
||
// 处理设备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(`[设备更换] 用户 ${maskPhone(phone)} 更换设备: ${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 { calculateRemainingDays } = require('../utils/account_utils');
|
||
const remaining_days = calculateRemainingDays(authDate, authDays);
|
||
|
||
const safeUserInfo = {
|
||
id: user.id,
|
||
sn_code: user.sn_code,
|
||
login_name: maskPhone(user.login_name), // 脱敏处理
|
||
is_enabled: user.is_enabled,
|
||
authorization_date: user.authorization_date,
|
||
authorization_days: user.authorization_days,
|
||
remaining_days: remaining_days,
|
||
auto_deliver: user.auto_deliver,
|
||
deliver_config: user.deliver_config,
|
||
created_at: user.created_at,
|
||
updated_at: user.updated_at
|
||
};
|
||
|
||
// 记录成功登录
|
||
console.log(`[登录成功] 用户 ${maskPhone(phone)}, 剩余天数: ${remaining_days}`);
|
||
|
||
return ctx.success({
|
||
token,
|
||
device_id,
|
||
user: safeUserInfo
|
||
});
|
||
} catch (error) {
|
||
// 记录详细错误但不暴露给客户端
|
||
console.error('[登录异常]', {
|
||
error: error.message,
|
||
stack: error.stack,
|
||
phone: maskPhone(phone)
|
||
});
|
||
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 字段
|
||
await pla_account.update(
|
||
{
|
||
deliver_config: 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('保存投递配置失败');
|
||
}
|
||
}
|
||
} |