This commit is contained in:
张成
2025-12-16 15:55:42 +08:00
parent f3e6413bfe
commit 41e03daa50
7 changed files with 1077 additions and 28 deletions

View File

@@ -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 || '验证失败'
};
}
}

View File

@@ -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);
}
}
};

123
api/model/invite_record.js Normal file
View File

@@ -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;
};