11
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
const Framework = require("../../framework/node-core-framework.js");
|
||||
const dayjs = require('dayjs');
|
||||
const email_service = require('../services/email_service.js');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -21,26 +22,25 @@ module.exports = {
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - phone
|
||||
* - email
|
||||
* - password
|
||||
* - sms_code
|
||||
* - invite_code
|
||||
* - email_code
|
||||
* properties:
|
||||
* phone:
|
||||
* email:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* example: '13800138000'
|
||||
* description: 邮箱地址
|
||||
* example: 'user@example.com'
|
||||
* password:
|
||||
* type: string
|
||||
* description: 密码
|
||||
* example: 'password123'
|
||||
* sms_code:
|
||||
* email_code:
|
||||
* type: string
|
||||
* description: 短信验证码
|
||||
* description: 验证码
|
||||
* example: '123456'
|
||||
* invite_code:
|
||||
* type: string
|
||||
* description: 邀请码
|
||||
* description: 邀请码(选填)
|
||||
* example: 'INV123_ABC123'
|
||||
* responses:
|
||||
* 200:
|
||||
@@ -49,17 +49,17 @@ module.exports = {
|
||||
'POST /invite/register': async (ctx) => {
|
||||
try {
|
||||
const body = ctx.getBody();
|
||||
const { phone, password, sms_code, invite_code } = body;
|
||||
const { email, password, email_code, invite_code } = body;
|
||||
|
||||
// 验证参数
|
||||
if (!phone || !password || !sms_code || !invite_code) {
|
||||
return ctx.fail('手机号、密码、短信验证码和邀请码不能为空');
|
||||
// 验证必填参数
|
||||
if (!email || !password || !email_code) {
|
||||
return ctx.fail('邮箱、密码和验证码不能为空');
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
return ctx.fail('手机号格式不正确');
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return ctx.fail('邮箱格式不正确');
|
||||
}
|
||||
|
||||
// 验证密码长度
|
||||
@@ -67,51 +67,57 @@ module.exports = {
|
||||
return ctx.fail('密码长度不能少于6位');
|
||||
}
|
||||
|
||||
// 验证短信验证码(这里需要调用短信验证服务)
|
||||
const smsVerifyResult = await verifySmsCode(phone, sms_code);
|
||||
if (!smsVerifyResult.success) {
|
||||
return ctx.fail(smsVerifyResult.message || '短信验证码错误或已过期');
|
||||
// 验证验证码
|
||||
const emailVerifyResult = await verifyEmailCode(email, email_code);
|
||||
if (!emailVerifyResult.success) {
|
||||
return ctx.fail(emailVerifyResult.message || '验证码错误或已过期');
|
||||
}
|
||||
|
||||
const { pla_account } = await Framework.getModels();
|
||||
|
||||
// 检查手机号是否已注册
|
||||
// 检查邮箱是否已注册
|
||||
const existingUser = await pla_account.findOne({
|
||||
where: { login_name: phone }
|
||||
where: { login_name: email }
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return ctx.fail('该手机号已被注册');
|
||||
return ctx.fail('该邮箱已被注册');
|
||||
}
|
||||
|
||||
// 解析邀请码,获取邀请人ID
|
||||
// 邀请码格式:INV{user_id}_{timestamp}
|
||||
const inviteMatch = invite_code.match(/^INV(\d+)_/);
|
||||
if (!inviteMatch) {
|
||||
return ctx.fail('邀请码格式不正确');
|
||||
// 验证邀请码(如果提供了邀请码)
|
||||
let inviter = null;
|
||||
let inviter_id = null;
|
||||
|
||||
if (invite_code) {
|
||||
// 解析邀请码,获取邀请人ID
|
||||
// 邀请码格式:INV{user_id}_{timestamp}
|
||||
const inviteMatch = invite_code.match(/^INV(\d+)_/);
|
||||
if (!inviteMatch) {
|
||||
return ctx.fail('邀请码格式不正确');
|
||||
}
|
||||
|
||||
inviter_id = parseInt(inviteMatch[1]);
|
||||
|
||||
// 验证邀请人是否存在
|
||||
inviter = await pla_account.findOne({
|
||||
where: { id: inviter_id }
|
||||
});
|
||||
|
||||
if (!inviter) {
|
||||
return ctx.fail('邀请码无效,邀请人不存在');
|
||||
}
|
||||
}
|
||||
|
||||
const inviter_id = parseInt(inviteMatch[1]);
|
||||
|
||||
// 验证邀请人是否存在
|
||||
const inviter = await pla_account.findOne({
|
||||
where: { id: inviter_id }
|
||||
});
|
||||
|
||||
if (!inviter) {
|
||||
return ctx.fail('邀请码无效,邀请人不存在');
|
||||
}
|
||||
|
||||
// 生成设备SN码(基于手机号和时间戳)
|
||||
// 生成设备SN码(基于邮箱和时间戳)
|
||||
const sn_code = `SN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||||
|
||||
// 创建新用户
|
||||
const newUser = await pla_account.create({
|
||||
name: phone, // 默认使用手机号作为名称
|
||||
name: email.split('@')[0], // 默认使用邮箱用户名作为名称
|
||||
sn_code: sn_code,
|
||||
device_id: '', // 设备ID由客户端登录时提供
|
||||
platform_type: 'boss', // 默认平台类型
|
||||
login_name: phone,
|
||||
login_name: email,
|
||||
pwd: password,
|
||||
keyword: '',
|
||||
is_enabled: 1,
|
||||
@@ -158,7 +164,7 @@ module.exports = {
|
||||
inviter_sn_code: inviter.sn_code,
|
||||
invitee_id: newUser.id,
|
||||
invitee_sn_code: newUser.sn_code,
|
||||
invitee_phone: phone,
|
||||
invitee_phone: email, // 使用邮箱代替手机号
|
||||
invite_code: invite_code,
|
||||
register_time: new Date(),
|
||||
reward_status: 1, // 已发放
|
||||
@@ -167,7 +173,9 @@ module.exports = {
|
||||
is_delete: 0
|
||||
});
|
||||
|
||||
console.log(`[邀请注册] 用户 ${phone} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`);
|
||||
console.log(`[邀请注册] 用户 ${email} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`);
|
||||
} else {
|
||||
console.log(`[邀请注册] 用户 ${email} 注册成功(无邀请码)`);
|
||||
}
|
||||
|
||||
return ctx.success({
|
||||
@@ -186,10 +194,10 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /admin_api/invite/send-sms:
|
||||
* /admin_api/invite/send-email-code:
|
||||
* post:
|
||||
* summary: 发送短信验证码
|
||||
* description: 发送短信验证码到指定手机号
|
||||
* summary: 发送验证码
|
||||
* description: 发送验证码到指定邮箱地址
|
||||
* tags: [后台-邀请注册]
|
||||
* requestBody:
|
||||
* required: true
|
||||
@@ -198,107 +206,114 @@ module.exports = {
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - phone
|
||||
* - email
|
||||
* properties:
|
||||
* phone:
|
||||
* email:
|
||||
* type: string
|
||||
* description: 手机号
|
||||
* example: '13800138000'
|
||||
* description: 邮箱地址
|
||||
* example: 'user@example.com'
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 发送成功
|
||||
*/
|
||||
'POST /invite/send-sms': async (ctx) => {
|
||||
'POST /invite/send-email-code': async (ctx) => {
|
||||
try {
|
||||
const body = ctx.getBody();
|
||||
const { phone } = body;
|
||||
const { email } = body;
|
||||
|
||||
if (!phone) {
|
||||
return ctx.fail('手机号不能为空');
|
||||
if (!email) {
|
||||
return ctx.fail('邮箱地址不能为空');
|
||||
}
|
||||
|
||||
// 验证手机号格式
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phoneRegex.test(phone)) {
|
||||
return ctx.fail('手机号格式不正确');
|
||||
// 验证邮箱格式
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return ctx.fail('邮箱格式不正确');
|
||||
}
|
||||
|
||||
// 发送短信验证码
|
||||
const smsResult = await sendSmsCode(phone);
|
||||
if (!smsResult.success) {
|
||||
return ctx.fail(smsResult.message || '发送短信验证码失败');
|
||||
// 发送验证码
|
||||
const emailResult = await sendEmailCode(email);
|
||||
if (!emailResult.success) {
|
||||
return ctx.fail(emailResult.message || '发送验证码失败');
|
||||
}
|
||||
|
||||
return ctx.success({
|
||||
message: '短信验证码已发送',
|
||||
expire_time: smsResult.expire_time || 300 // 默认5分钟过期
|
||||
message: '验证码已发送',
|
||||
expire_time: emailResult.expire_time || 300 // 默认5分钟过期
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('发送短信验证码失败:', error);
|
||||
return ctx.fail('发送短信验证码失败: ' + error.message);
|
||||
console.error('发送验证码失败:', error);
|
||||
return ctx.fail('发送验证码失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
* @param {string} phone 手机号
|
||||
* 发送验证码
|
||||
* @param {string} email 邮箱地址
|
||||
* @returns {Promise<{success: boolean, message?: string, expire_time?: number}>}
|
||||
*/
|
||||
async function sendSmsCode(phone) {
|
||||
async function sendEmailCode(email) {
|
||||
try {
|
||||
// TODO: 实现真实的短信发送逻辑
|
||||
// 这里可以使用第三方短信服务(如阿里云、腾讯云等)
|
||||
|
||||
// 生成6位随机验证码
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
|
||||
// 将验证码存储到缓存中(可以使用Redis或内存缓存)
|
||||
// 格式:sms_code:{phone} = {code, expire_time}
|
||||
// 格式:email_code:{email} = {code, expire_time}
|
||||
const expire_time = Date.now() + 5 * 60 * 1000; // 5分钟后过期
|
||||
|
||||
// 这里应该存储到缓存中,暂时使用全局变量(生产环境应使用Redis)
|
||||
if (!global.smsCodeCache) {
|
||||
global.smsCodeCache = {};
|
||||
if (!global.emailCodeCache) {
|
||||
global.emailCodeCache = {};
|
||||
}
|
||||
global.smsCodeCache[phone] = {
|
||||
global.emailCodeCache[email] = {
|
||||
code: code,
|
||||
expire_time: expire_time
|
||||
};
|
||||
|
||||
// TODO: 调用真实的短信发送接口
|
||||
console.log(`[短信验证] 发送验证码到 ${phone}: ${code} (5分钟内有效)`);
|
||||
// 调用邮件服务发送验证码
|
||||
const email_result = await email_service.send_verification_code(email, code);
|
||||
|
||||
if (!email_result.success) {
|
||||
// 如果邮件发送失败,删除已生成的验证码
|
||||
delete global.emailCodeCache[email];
|
||||
return {
|
||||
success: false,
|
||||
message: email_result.message || '发送验证码失败'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`[邮箱验证] 验证码已发送到 ${email}: ${code} (5分钟内有效)`);
|
||||
|
||||
// 模拟发送成功
|
||||
return {
|
||||
success: true,
|
||||
expire_time: 300
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('发送短信验证码失败:', error);
|
||||
console.error('发送验证码失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '发送短信验证码失败'
|
||||
message: error.message || '发送验证码失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
* @param {string} phone 手机号
|
||||
* 验证验证码
|
||||
* @param {string} email 邮箱地址
|
||||
* @param {string} code 验证码
|
||||
* @returns {Promise<{success: boolean, message?: string}>}
|
||||
*/
|
||||
async function verifySmsCode(phone, code) {
|
||||
async function verifyEmailCode(email, code) {
|
||||
try {
|
||||
if (!global.smsCodeCache) {
|
||||
if (!global.emailCodeCache) {
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码不存在或已过期'
|
||||
};
|
||||
}
|
||||
|
||||
const cached = global.smsCodeCache[phone];
|
||||
const cached = global.emailCodeCache[email];
|
||||
if (!cached) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -308,7 +323,7 @@ async function verifySmsCode(phone, code) {
|
||||
|
||||
// 检查是否过期
|
||||
if (Date.now() > cached.expire_time) {
|
||||
delete global.smsCodeCache[phone];
|
||||
delete global.emailCodeCache[email];
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码已过期,请重新获取'
|
||||
@@ -324,13 +339,13 @@ async function verifySmsCode(phone, code) {
|
||||
}
|
||||
|
||||
// 验证成功后删除缓存
|
||||
delete global.smsCodeCache[phone];
|
||||
delete global.emailCodeCache[email];
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('验证短信验证码失败:', error);
|
||||
console.error('验证验证码失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '验证失败'
|
||||
|
||||
189
api/services/email_service.js
Normal file
189
api/services/email_service.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 邮件服务
|
||||
* 使用QQ邮箱SMTP服务发送邮件
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const config = require('../../config/config.js');
|
||||
|
||||
// 创建邮件传输器
|
||||
let transporter = null;
|
||||
|
||||
/**
|
||||
* 初始化邮件传输器
|
||||
*/
|
||||
function init_transporter() {
|
||||
if (transporter) {
|
||||
return transporter;
|
||||
}
|
||||
|
||||
// QQ邮箱SMTP配置
|
||||
const email_config = config.email || {
|
||||
host: 'smtp.qq.com',
|
||||
port: 465,
|
||||
secure: true, // 使用SSL
|
||||
auth: {
|
||||
user: process.env.QQ_EMAIL_USER || '', // QQ邮箱账号
|
||||
pass: process.env.QQ_EMAIL_PASS || '' // QQ邮箱授权码(不是密码)
|
||||
}
|
||||
};
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: email_config.host,
|
||||
port: email_config.port,
|
||||
secure: email_config.secure,
|
||||
auth: email_config.auth
|
||||
});
|
||||
|
||||
return transporter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮件
|
||||
* @param {Object} options 邮件选项
|
||||
* @param {string} options.to 收件人邮箱
|
||||
* @param {string} options.subject 邮件主题
|
||||
* @param {string} options.html 邮件HTML内容
|
||||
* @param {string} options.text 邮件纯文本内容(可选)
|
||||
* @returns {Promise<{success: boolean, message?: string, messageId?: string}>}
|
||||
*/
|
||||
async function send_email(options) {
|
||||
try {
|
||||
const transporter_instance = init_transporter();
|
||||
|
||||
if (!transporter_instance) {
|
||||
return {
|
||||
success: false,
|
||||
message: '邮件服务未配置'
|
||||
};
|
||||
}
|
||||
|
||||
// 如果没有配置邮箱账号,返回错误
|
||||
const email_config = config.email || {};
|
||||
if (!email_config.auth || !email_config.auth.user || !email_config.auth.pass) {
|
||||
console.warn('[邮件服务] QQ邮箱未配置,使用模拟发送');
|
||||
// 开发环境可以模拟发送
|
||||
if (config.env === 'development') {
|
||||
console.log(`[模拟邮件] 发送到 ${options.to}`);
|
||||
console.log(`[模拟邮件] 主题: ${options.subject}`);
|
||||
console.log(`[模拟邮件] 内容: ${options.text || options.html}`);
|
||||
return {
|
||||
success: true,
|
||||
message: '邮件已发送(模拟)',
|
||||
messageId: 'mock-' + Date.now()
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
message: '邮件服务未配置,请联系管理员'
|
||||
};
|
||||
}
|
||||
|
||||
// 发送邮件
|
||||
const mail_options = {
|
||||
from: `"${email_config.fromName || '自动找工作系统'}" <${email_config.auth.user}>`,
|
||||
to: options.to,
|
||||
subject: options.subject,
|
||||
html: options.html,
|
||||
text: options.text || options.html.replace(/<[^>]*>/g, '') // 如果没有text,从html提取
|
||||
};
|
||||
|
||||
const info = await transporter_instance.sendMail(mail_options);
|
||||
|
||||
console.log(`[邮件服务] 邮件发送成功: ${options.to}, MessageId: ${info.messageId}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '邮件发送成功',
|
||||
messageId: info.messageId
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[邮件服务] 发送邮件失败:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || '发送邮件失败'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送验证码邮件
|
||||
* @param {string} email 收件人邮箱
|
||||
* @param {string} code 验证码
|
||||
* @returns {Promise<{success: boolean, message?: string, messageId?: string}>}
|
||||
*/
|
||||
async function send_verification_code(email, code) {
|
||||
const subject = '【自动找工作系统】注册验证码';
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.content {
|
||||
background: #ffffff;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.code-box {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 8px;
|
||||
}
|
||||
.tip {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<h2>验证码</h2>
|
||||
<p>您好,</p>
|
||||
<p>您正在注册自动找工作系统,验证码为:</p>
|
||||
<div class="code-box">${code}</div>
|
||||
<p>验证码有效期为 <strong>5分钟</strong>,请勿泄露给他人。</p>
|
||||
<div class="tip">
|
||||
<p>如果这不是您的操作,请忽略此邮件。</p>
|
||||
<p>此邮件由系统自动发送,请勿回复。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
return await send_email({
|
||||
to: email,
|
||||
subject: subject,
|
||||
html: html
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
send_email,
|
||||
send_verification_code,
|
||||
init_transporter
|
||||
};
|
||||
Reference in New Issue
Block a user