From 41e03daa50c1c4b6e42bb14b2f4d7099b7556870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E6=88=90?= Date: Tue, 16 Dec 2025 15:55:42 +0800 Subject: [PATCH] 1 --- .../src/api/invite/invite_register_server.js | 38 ++ admin/src/router/component-map.js | 4 + admin/src/views/invite/invite_register.vue | 359 ++++++++++++++++++ api/controller_admin/invite_register.js | 340 +++++++++++++++++ api/controller_front/invite.js | 239 ++++++++++-- api/model/invite_record.js | 123 ++++++ config/config.js | 2 +- 7 files changed, 1077 insertions(+), 28 deletions(-) create mode 100644 admin/src/api/invite/invite_register_server.js create mode 100644 admin/src/views/invite/invite_register.vue create mode 100644 api/controller_admin/invite_register.js create mode 100644 api/model/invite_record.js diff --git a/admin/src/api/invite/invite_register_server.js b/admin/src/api/invite/invite_register_server.js new file mode 100644 index 0000000..f12d342 --- /dev/null +++ b/admin/src/api/invite/invite_register_server.js @@ -0,0 +1,38 @@ +/** + * 邀请注册 API 服务 + * 封装邀请注册相关的API调用 + */ + +class InviteRegisterServer { + /** + * 发送短信验证码 + * @param {string} phone 手机号 + * @returns {Promise} + */ + sendSmsCode(phone) { + return window.framework.http.post('/invite/send-sms', { + phone: phone + }); + } + + /** + * 邀请注册 + * @param {Object} params 注册参数 + * @param {string} params.phone 手机号 + * @param {string} params.password 密码 + * @param {string} params.sms_code 短信验证码 + * @param {string} params.invite_code 邀请码 + * @returns {Promise} + */ + register(params) { + return window.framework.http.post('/invite/register', { + phone: params.phone, + password: params.password, + sms_code: params.sms_code, + invite_code: params.invite_code + }); + } +} + +export default new InviteRegisterServer(); + diff --git a/admin/src/router/component-map.js b/admin/src/router/component-map.js index 7ea7c04..c9918bd 100644 --- a/admin/src/router/component-map.js +++ b/admin/src/router/component-map.js @@ -25,6 +25,9 @@ import JobTypes from '@/views/work/job_types.vue' // 首页模块 import HomeIndex from '@/views/home/index.vue' +// 邀请注册模块 +import InviteRegister from '@/views/invite/invite_register.vue' + /** @@ -53,6 +56,7 @@ 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 new file mode 100644 index 0000000..f968d19 --- /dev/null +++ b/admin/src/views/invite/invite_register.vue @@ -0,0 +1,359 @@ + + + + + + diff --git a/api/controller_admin/invite_register.js b/api/controller_admin/invite_register.js new file mode 100644 index 0000000..eb87855 --- /dev/null +++ b/api/controller_admin/invite_register.js @@ -0,0 +1,340 @@ +/** + * 邀请注册管理控制器(后台管理) + * 提供邀请注册相关的接口,不需要登录验证 + */ + +const Framework = require("../../framework/node-core-framework.js"); +const dayjs = require('dayjs'); + +module.exports = { + /** + * @swagger + * /admin_api/invite/register: + * post: + * summary: 邀请注册 + * description: 通过邀请码注册新用户,注册成功后给邀请人增加3天试用期 + * tags: [后台-邀请注册] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - phone + * - password + * - sms_code + * - invite_code + * properties: + * phone: + * type: string + * description: 手机号 + * example: '13800138000' + * password: + * type: string + * description: 密码 + * example: 'password123' + * sms_code: + * type: string + * description: 短信验证码 + * example: '123456' + * invite_code: + * type: string + * description: 邀请码 + * example: 'INV123_ABC123' + * responses: + * 200: + * description: 注册成功 + */ + 'POST /invite/register': async (ctx) => { + try { + const body = ctx.getBody(); + const { phone, password, sms_code, invite_code } = body; + + // 验证参数 + if (!phone || !password || !sms_code || !invite_code) { + return ctx.fail('手机号、密码、短信验证码和邀请码不能为空'); + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + return ctx.fail('手机号格式不正确'); + } + + // 验证密码长度 + if (password.length < 6) { + return ctx.fail('密码长度不能少于6位'); + } + + // 验证短信验证码(这里需要调用短信验证服务) + const smsVerifyResult = await verifySmsCode(phone, sms_code); + if (!smsVerifyResult.success) { + return ctx.fail(smsVerifyResult.message || '短信验证码错误或已过期'); + } + + const { pla_account } = await Framework.getModels(); + + // 检查手机号是否已注册 + const existingUser = await pla_account.findOne({ + where: { login_name: phone } + }); + + if (existingUser) { + return ctx.fail('该手机号已被注册'); + } + + // 解析邀请码,获取邀请人ID + // 邀请码格式:INV{user_id}_{timestamp} + const inviteMatch = invite_code.match(/^INV(\d+)_/); + if (!inviteMatch) { + 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码(基于手机号和时间戳) + const sn_code = `SN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`; + + // 创建新用户 + const newUser = await pla_account.create({ + name: phone, // 默认使用手机号作为名称 + sn_code: sn_code, + device_id: '', // 设备ID由客户端登录时提供 + platform_type: 'boss', // 默认平台类型 + login_name: phone, + pwd: password, + keyword: '', + is_enabled: 1, + is_delete: 0, + authorization_date: null, + authorization_days: 0 + }); + + // 给邀请人增加3天试用期 + if (inviter) { + const inviterData = inviter.toJSON(); + const currentAuthDate = inviterData.authorization_date; + const currentAuthDays = inviterData.authorization_days || 0; + + let newAuthDate = currentAuthDate; + let newAuthDays = currentAuthDays + 3; // 增加3天 + + // 如果当前没有授权日期,则从今天开始 + if (!currentAuthDate) { + newAuthDate = new Date(); + } else { + // 如果当前授权已过期,从今天开始计算 + const currentEndDate = dayjs(currentAuthDate).add(currentAuthDays, 'day'); + const now = dayjs(); + if (currentEndDate.isBefore(now)) { + newAuthDate = new Date(); + newAuthDays = 3; // 重新设置为3天 + } + } + + // 更新邀请人的授权信息 + await pla_account.update( + { + authorization_date: newAuthDate, + authorization_days: newAuthDays + }, + { where: { id: inviter_id } } + ); + + // 记录邀请记录 + const { invite_record } = await Framework.getModels(); + await invite_record.create({ + inviter_id: inviter_id, + inviter_sn_code: inviter.sn_code, + invitee_id: newUser.id, + invitee_sn_code: newUser.sn_code, + invitee_phone: phone, + invite_code: invite_code, + register_time: new Date(), + reward_status: 1, // 已发放 + reward_type: 'trial_days', + reward_value: 3, + is_delete: 0 + }); + + console.log(`[邀请注册] 用户 ${phone} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`); + } + + return ctx.success({ + message: '注册成功', + user: { + id: newUser.id, + sn_code: newUser.sn_code, + login_name: newUser.login_name + } + }); + } catch (error) { + console.error('邀请注册失败:', error); + return ctx.fail('注册失败: ' + error.message); + } + }, + + /** + * @swagger + * /admin_api/invite/send-sms: + * post: + * summary: 发送短信验证码 + * description: 发送短信验证码到指定手机号 + * tags: [后台-邀请注册] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - phone + * properties: + * phone: + * type: string + * description: 手机号 + * example: '13800138000' + * responses: + * 200: + * description: 发送成功 + */ + 'POST /invite/send-sms': async (ctx) => { + try { + const body = ctx.getBody(); + const { phone } = body; + + if (!phone) { + return ctx.fail('手机号不能为空'); + } + + // 验证手机号格式 + const phoneRegex = /^1[3-9]\d{9}$/; + if (!phoneRegex.test(phone)) { + return ctx.fail('手机号格式不正确'); + } + + // 发送短信验证码 + const smsResult = await sendSmsCode(phone); + if (!smsResult.success) { + return ctx.fail(smsResult.message || '发送短信验证码失败'); + } + + return ctx.success({ + message: '短信验证码已发送', + expire_time: smsResult.expire_time || 300 // 默认5分钟过期 + }); + } catch (error) { + console.error('发送短信验证码失败:', error); + return ctx.fail('发送短信验证码失败: ' + error.message); + } + } +}; + +/** + * 发送短信验证码 + * @param {string} phone 手机号 + * @returns {Promise<{success: boolean, message?: string, expire_time?: number}>} + */ +async function sendSmsCode(phone) { + try { + // TODO: 实现真实的短信发送逻辑 + // 这里可以使用第三方短信服务(如阿里云、腾讯云等) + + // 生成6位随机验证码 + const code = Math.floor(100000 + Math.random() * 900000).toString(); + + // 将验证码存储到缓存中(可以使用Redis或内存缓存) + // 格式:sms_code:{phone} = {code, expire_time} + const expire_time = Date.now() + 5 * 60 * 1000; // 5分钟后过期 + + // 这里应该存储到缓存中,暂时使用全局变量(生产环境应使用Redis) + if (!global.smsCodeCache) { + global.smsCodeCache = {}; + } + global.smsCodeCache[phone] = { + code: code, + expire_time: expire_time + }; + + // TODO: 调用真实的短信发送接口 + console.log(`[短信验证] 发送验证码到 ${phone}: ${code} (5分钟内有效)`); + + // 模拟发送成功 + return { + success: true, + expire_time: 300 + }; + } catch (error) { + console.error('发送短信验证码失败:', error); + return { + success: false, + message: error.message || '发送短信验证码失败' + }; + } +} + +/** + * 验证短信验证码 + * @param {string} phone 手机号 + * @param {string} code 验证码 + * @returns {Promise<{success: boolean, message?: string}>} + */ +async function verifySmsCode(phone, code) { + try { + if (!global.smsCodeCache) { + return { + success: false, + message: '验证码不存在或已过期' + }; + } + + const cached = global.smsCodeCache[phone]; + if (!cached) { + return { + success: false, + message: '验证码不存在或已过期' + }; + } + + // 检查是否过期 + if (Date.now() > cached.expire_time) { + delete global.smsCodeCache[phone]; + return { + success: false, + message: '验证码已过期,请重新获取' + }; + } + + // 验证码是否正确 + if (cached.code !== code) { + return { + success: false, + message: '验证码错误' + }; + } + + // 验证成功后删除缓存 + delete global.smsCodeCache[phone]; + + return { + success: true + }; + } catch (error) { + console.error('验证短信验证码失败:', error); + return { + success: false, + message: error.message || '验证失败' + }; + } +} + diff --git a/api/controller_front/invite.js b/api/controller_front/invite.js index 06e2801..d101c96 100644 --- a/api/controller_front/invite.js +++ b/api/controller_front/invite.js @@ -9,32 +9,59 @@ module.exports = { /** * @swagger * /api/invite/info: - * get: + * post: * summary: 获取邀请信息 - * description: 根据用户ID获取邀请码和邀请链接 + * description: 根据设备SN码获取邀请码和邀请链接 * tags: [前端-推广邀请] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sn_code + * properties: + * sn_code: + * type: string + * description: 设备SN码 * responses: * 200: * description: 获取成功 */ - 'GET /invite/info': async (ctx) => { + 'POST /invite/info': async (ctx) => { try { - // 从query或body中获取user_id,实际应该从token中解析 + // 从body中获取sn_code const body = ctx.getBody(); - const user_id = body.user_id || ctx.query?.user_id; + const { sn_code } = body; - if (!user_id) { - return ctx.fail('请先登录'); + if (!sn_code) { + return ctx.fail('请提供设备SN码'); } - // 生成邀请码(基于用户ID) - const invite_code = `INV${user_id}${Date.now().toString(36).toUpperCase()}`; + const { pla_account } = await Framework.getModels(); + + // 根据sn_code查找用户 + const user = await pla_account.findOne({ + where: { sn_code } + }); + + if (!user) { + return ctx.fail('用户不存在'); + } + + // 生成邀请码(基于用户ID和sn_code) + const invite_code = `INV${user.id}_${Date.now().toString(36).toUpperCase()}`; const invite_link = `https://work.light120.com/invite?code=${invite_code}`; + // 保存邀请码到用户记录(可以保存到invite_code字段,如果没有则保存到备注或其他字段) + // 这里暂时不保存到数据库,每次生成新的 + return ctx.success({ invite_code, invite_link, - user_id + user_id: user.id, + sn_code: user.sn_code }); } catch (error) { console.error('获取邀请信息失败:', error); @@ -45,30 +72,83 @@ module.exports = { /** * @swagger * /api/invite/statistics: - * get: + * post: * summary: 获取邀请统计 - * description: 根据用户ID获取邀请统计数据 + * description: 根据设备SN码获取邀请统计数据 * tags: [前端-推广邀请] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sn_code + * properties: + * sn_code: + * type: string + * description: 设备SN码 * responses: * 200: * description: 获取成功 */ - 'GET /invite/statistics': async (ctx) => { + 'POST /invite/statistics': async (ctx) => { try { - // 从query或body中获取user_id,实际应该从token中解析 + // 从body中获取sn_code const body = ctx.getBody() || {}; - const user_id = body.user_id || ctx.query?.user_id; + const { sn_code } = body; - if (!user_id) { - return ctx.fail('请先登录'); + 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('用户不存在'); + } + + // 查询邀请统计 + const { invite_record } = await Framework.getModels(); + + // 查询总邀请数 + const totalInvites = await invite_record.count({ + where: { + inviter_id: user.id, + is_delete: 0 + } + }); + + // 查询已发放奖励的邀请数(活跃邀请) + const activeInvites = await invite_record.count({ + where: { + inviter_id: user.id, + reward_status: 1, + is_delete: 0 + } + }); + + // 查询总奖励天数 + const rewardRecords = await invite_record.findAll({ + where: { + inviter_id: user.id, + reward_status: 1, + is_delete: 0 + }, + attributes: ['reward_value'] + }); + + const totalRewardDays = rewardRecords.reduce((sum, record) => { + return sum + (record.reward_value || 0); + }, 0); + return ctx.success({ - totalInvites: 0, - activeInvites: 0, - rewardPoints: 0, - rewardAmount: 0 + totalInvites: totalInvites || 0 }); } catch (error) { console.error('获取邀请统计失败:', error); @@ -89,16 +169,27 @@ module.exports = { */ 'POST /invite/generate': async (ctx) => { try { - // 从query或body中获取user_id,实际应该从token中解析 + // 从body中获取sn_code const body = ctx.getBody() || {}; - const user_id = body.user_id || ctx.query?.user_id; + const { sn_code } = body; - if (!user_id) { - return ctx.fail('请先登录'); + 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('用户不存在'); } // 生成新的邀请码 - const invite_code = `INV${user_id}${Date.now().toString(36).toUpperCase()}`; + const invite_code = `INV${user.id}_${Date.now().toString(36).toUpperCase()}`; const invite_link = `https://work.light120.com/invite?code=${invite_code}`; return ctx.success({ @@ -109,6 +200,100 @@ module.exports = { console.error('生成邀请码失败:', error); return ctx.fail('生成邀请码失败: ' + error.message); } + }, + + /** + * @swagger + * /api/invite/records: + * post: + * summary: 获取邀请记录列表 + * description: 根据设备SN码获取邀请记录列表 + * tags: [前端-推广邀请] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - sn_code + * properties: + * sn_code: + * type: string + * description: 设备SN码 + * page: + * type: integer + * description: 页码(可选,默认1) + * pageSize: + * type: integer + * description: 每页数量(可选,默认20) + * responses: + * 200: + * description: 获取成功 + */ + 'POST /invite/records': async (ctx) => { + try { + const body = ctx.getBody() || {}; + const { sn_code, page = 1, pageSize = 20 } = body; + + if (!sn_code) { + return ctx.fail('请提供设备SN码'); + } + + const { pla_account, invite_record } = await Framework.getModels(); + + // 根据sn_code查找用户 + const user = await pla_account.findOne({ + where: { sn_code } + }); + + if (!user) { + return ctx.fail('用户不存在'); + } + + // 查询邀请记录列表 + const offset = (page - 1) * pageSize; + const records = await invite_record.findAll({ + where: { + inviter_id: user.id, + is_delete: 0 + }, + order: [['register_time', 'DESC']], + limit: pageSize, + offset: offset + }); + + const total = await invite_record.count({ + where: { + inviter_id: user.id, + is_delete: 0 + } + }); + + // 格式化记录数据 + const formattedRecords = records.map(record => { + const recordData = record.toJSON(); + return { + id: recordData.id, + invitee_phone: recordData.invitee_phone, + invite_code: recordData.invite_code, + register_time: recordData.register_time, + reward_status: recordData.reward_status, + reward_value: recordData.reward_value, + reward_type: recordData.reward_type + }; + }); + + return ctx.success({ + list: formattedRecords, + total: total, + page: page, + pageSize: pageSize + }); + } catch (error) { + console.error('获取邀请记录列表失败:', error); + return ctx.fail('获取邀请记录列表失败: ' + error.message); + } } }; diff --git a/api/model/invite_record.js b/api/model/invite_record.js new file mode 100644 index 0000000..9c8efb3 --- /dev/null +++ b/api/model/invite_record.js @@ -0,0 +1,123 @@ +/** + * 邀请记录表模型 + * 记录用户邀请注册的信息 + */ + +const Sequelize = require('sequelize'); + +module.exports = (db) => { + const invite_record = db.define("invite_record", { + // 邀请人ID(关联 pla_account.id) + inviter_id: { + comment: '邀请人ID', + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'pla_account', + key: 'id' + } + }, + // 邀请人SN码 + inviter_sn_code: { + comment: '邀请人SN码', + type: Sequelize.STRING(50), + allowNull: false, + defaultValue: '' + }, + // 被邀请人ID(关联 pla_account.id) + invitee_id: { + comment: '被邀请人ID', + type: Sequelize.INTEGER, + allowNull: false, + references: { + model: 'pla_account', + key: 'id' + } + }, + // 被邀请人SN码 + invitee_sn_code: { + comment: '被邀请人SN码', + type: Sequelize.STRING(50), + allowNull: false, + defaultValue: '' + }, + // 被邀请人手机号 + invitee_phone: { + comment: '被邀请人手机号', + type: Sequelize.STRING(20), + allowNull: false, + defaultValue: '' + }, + // 邀请码 + invite_code: { + comment: '邀请码', + type: Sequelize.STRING(100), + allowNull: false, + defaultValue: '' + }, + // 注册时间 + register_time: { + comment: '注册时间', + type: Sequelize.DATE, + allowNull: false, + defaultValue: Sequelize.NOW + }, + // 奖励状态(0=未发放,1=已发放) + reward_status: { + comment: '奖励状态(0=未发放,1=已发放)', + type: Sequelize.TINYINT(1), + allowNull: false, + defaultValue: 0 + }, + // 奖励类型(trial_days=试用期天数) + reward_type: { + comment: '奖励类型', + type: Sequelize.STRING(50), + allowNull: false, + defaultValue: 'trial_days' + }, + // 奖励值(试用期天数) + reward_value: { + comment: '奖励值(试用期天数)', + type: Sequelize.INTEGER, + allowNull: false, + defaultValue: 3 + }, + + // 备注 + remark: { + comment: '备注', + type: Sequelize.TEXT, + allowNull: true + } + }, { + tableName: 'invite_record', + indexes: [ + { + name: 'idx_inviter_id', + fields: ['inviter_id'] + }, + { + name: 'idx_invitee_id', + fields: ['invitee_id'] + }, + { + name: 'idx_invite_code', + fields: ['invite_code'] + }, + { + name: 'idx_register_time', + fields: ['register_time'] + }, + { + name: 'idx_reward_status', + fields: ['reward_status'] + } + ] + }); + + // invite_record.sync({ alter: true }); + + return invite_record; +}; + diff --git a/config/config.js b/config/config.js index 800545b..c9a608d 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'], + "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'], // AI服务配置