From cfbcbc39fd440fc3b2f1b528940ecbbdee09a4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Fri, 19 Dec 2025 15:54:52 +0800 Subject: [PATCH] 11 --- admin/public/register.html | 762 +++++++++++++++++++++ admin/src/router/component-map.js | 5 +- admin/src/views/invite/invite_register.vue | 359 ---------- admin/webpack.config.js | 27 +- api/controller_admin/invite_register.js | 197 +++--- api/services/email_service.js | 189 +++++ config/config.js | 13 +- package.json | 1 + 8 files changed, 1097 insertions(+), 456 deletions(-) create mode 100644 admin/public/register.html delete mode 100644 admin/src/views/invite/invite_register.vue create mode 100644 api/services/email_service.js diff --git a/admin/public/register.html b/admin/public/register.html new file mode 100644 index 0000000..7d232f0 --- /dev/null +++ b/admin/public/register.html @@ -0,0 +1,762 @@ + + + + + + 邀请注册 + + + +
+
+

🎉 邀请注册

+

填写信息完成注册,邀请人将获得奖励

+ +
+ +
+
+ +
+
+ + +
+
+ +
+ +
+ + +
+
+
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+ + +
+ +
+

注册说明

+
    +
  • 邮箱将作为您的登录账号
  • +
  • 密码长度至少6位,建议使用字母+数字组合
  • +
  • 邮箱验证码有效期为5分钟
  • +
  • 邀请码为选填,填写邀请码注册成功后,邀请人将获得3天试用期奖励
  • +
  • 请妥善保管您的账号信息
  • +
+
+
+ + + + diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index c9918bd..56fe105 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -25,8 +25,6 @@ import JobTypes from '@/views/work/job_types.vue' // 首页模块 import HomeIndex from '@/views/home/index.vue' -// 邀请注册模块 -import InviteRegister from '@/views/invite/invite_register.vue' @@ -56,7 +54,8 @@ const componentMap = { 'system/version': Version, 'work/job_types': JobTypes, 'home/index': HomeIndex, - 'invite/invite_register': InviteRegister, + + } export default componentMap diff --git a/admin/src/views/invite/invite_register.vue b/admin/src/views/invite/invite_register.vue deleted file mode 100644 index 1a8810d..0000000 --- a/admin/src/views/invite/invite_register.vue +++ /dev/null @@ -1,359 +0,0 @@ - - - - - - diff --git a/admin/webpack.config.js b/admin/webpack.config.js index e3af3a3..c8f788a 100644 --- a/admin/webpack.config.js +++ b/admin/webpack.config.js @@ -7,7 +7,9 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin') const is_production = process.env.NODE_ENV === 'production' || process.argv.includes('--mode=production') module.exports = { - entry: './src/main.js', + entry: { + main: './src/main.js' + }, output: { path: path.resolve(__dirname, 'dist'), filename: 'js/[name].[contenthash:8].js', @@ -50,9 +52,30 @@ module.exports = { }, plugins: [ new VueLoaderPlugin(), + // 主应用 HTML new HtmlWebpackPlugin({ template: './public/index.html', - title: 'Admin Framework Demo' + title: 'Admin Framework Demo', + filename: 'index.html', + chunks: ['main'] + }), + // 注册页面 HTML(独立页面,不依赖 webpack) + new HtmlWebpackPlugin({ + template: './public/register.html', + title: '邀请注册', + filename: 'register.html', + inject: false, // 不注入 webpack 生成的脚本 + minify: is_production ? { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true + } : false }), ...(is_production ? [ new MiniCssExtractPlugin({ diff --git a/api/controller_admin/invite_register.js b/api/controller_admin/invite_register.js index eb87855..04f825b 100644 --- a/api/controller_admin/invite_register.js +++ b/api/controller_admin/invite_register.js @@ -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 || '验证失败' diff --git a/api/services/email_service.js b/api/services/email_service.js new file mode 100644 index 0000000..db008e3 --- /dev/null +++ b/api/services/email_service.js @@ -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 = ` + + + + + + + +
+
+

验证码

+

您好,

+

您正在注册自动找工作系统,验证码为:

+
${code}
+

验证码有效期为 5分钟,请勿泄露给他人。

+
+

如果这不是您的操作,请忽略此邮件。

+

此邮件由系统自动发送,请勿回复。

+
+
+
+ + + `; + + return await send_email({ + to: email, + subject: subject, + html: html + }); +} + +module.exports = { + send_email, + send_verification_code, + init_transporter +}; diff --git a/config/config.js b/config/config.js index 8f34850..43eea6c 100644 --- a/config/config.js +++ b/config/config.js @@ -64,7 +64,7 @@ module.exports = { }, // 白名单URL - 不需要token验证的接口 - "allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify', '/api/version/check','/api/file/upload_file_to_oss_by_auto_work','/api/version/create', '/admin_api/invite/register', '/admin_api/invite/send-sms'], + "allowUrls": ["/admin_api/sys_user/login", "/admin_api/sys_user/authorityMenus", "/admin_api/sys_user/register", "/api/user/loginByWeixin", "/file/", "/sys_file/", "/admin_api/win_data/viewLogInfo", "/api/user/wx_auth", '/api/docs', 'api/swagger.json', 'payment/notify', 'payment/refund-notify', 'wallet/transfer_notify', 'user/sms/send', 'user/sms/verify', '/api/version/check','/api/file/upload_file_to_oss_by_auto_work','/api/version/create', '/admin_api/invite/register', '/admin_api/invite/send-email-code'], // AI服务配置 @@ -89,5 +89,16 @@ module.exports = { }, qq_map_key: "VIABZ-3N6HT-4BLXK-VF3FD-TM6YF-YRFQM", + // 邮件服务配置(QQ邮箱) + email: { + host: 'smtp.qq.com', + port: 465, + secure: true, // 使用SSL + fromName: '自动找工作系统', + auth: { + user: 'light603@qq.com' || '', // QQ邮箱账号,例如: 123456789@qq.com + pass: 'fxqnednoacqybbba' || '' // QQ邮箱授权码(不是密码,需要在QQ邮箱设置中获取) + } + } }; diff --git a/package.json b/package.json index 10cc4e8..5b336a7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mqtt": "^5.14.0", "mysql2": "^1.7.0", "node-schedule": "latest", + "nodemailer": "^6.9.7", "node-uuid": "^1.4.8", "redis": "^5.8.3", "sequelize": "^5.22.5",