557 lines
16 KiB
JavaScript
557 lines
16 KiB
JavaScript
/**
|
||
* 推广邀请管理控制器(客户端接口)
|
||
* 提供客户端调用的推广邀请相关接口
|
||
*/
|
||
|
||
const Framework = require("../../framework/node-core-framework.js");
|
||
const dayjs = require('dayjs');
|
||
const email_service = require('../services/email_service.js');
|
||
module.exports = {
|
||
/**
|
||
* @swagger
|
||
* /api/invite/info:
|
||
* post:
|
||
* summary: 获取邀请信息
|
||
* 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: 获取成功
|
||
*/
|
||
'POST /invite/info': async (ctx) => {
|
||
try {
|
||
// 从body中获取sn_code
|
||
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('用户不存在');
|
||
}
|
||
|
||
// 生成邀请码(基于用户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,
|
||
sn_code: user.sn_code
|
||
});
|
||
} catch (error) {
|
||
console.error('获取邀请信息失败:', error);
|
||
return ctx.fail('获取邀请信息失败: ' + error.message);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /api/invite/statistics:
|
||
* post:
|
||
* summary: 获取邀请统计
|
||
* 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: 获取成功
|
||
*/
|
||
'POST /invite/statistics': async (ctx) => {
|
||
try {
|
||
// 从body中获取sn_code
|
||
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('用户不存在');
|
||
}
|
||
|
||
// 查询邀请统计
|
||
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: totalInvites || 0
|
||
});
|
||
} catch (error) {
|
||
console.error('获取邀请统计失败:', error);
|
||
return ctx.fail('获取邀请统计失败: ' + error.message);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /api/invite/generate:
|
||
* post:
|
||
* summary: 生成邀请码
|
||
* description: 为用户生成新的邀请码
|
||
* tags: [前端-推广邀请]
|
||
* responses:
|
||
* 200:
|
||
* description: 生成成功
|
||
*/
|
||
'POST /invite/generate': async (ctx) => {
|
||
try {
|
||
// 从body中获取sn_code
|
||
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('用户不存在');
|
||
}
|
||
|
||
// 生成新的邀请码
|
||
const invite_code = `INV${user.id}_${Date.now().toString(36).toUpperCase()}`;
|
||
const invite_link = `https://work.light120.com/register.html?code=${invite_code}`;
|
||
|
||
return ctx.success({
|
||
invite_code,
|
||
invite_link
|
||
});
|
||
} catch (error) {
|
||
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);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* @swagger
|
||
* /api/invite/register:
|
||
* post:
|
||
* summary: 邀请注册
|
||
* description: 通过邀请码注册新用户,注册成功后给邀请人增加3天试用期
|
||
* tags: [前端-推广邀请]
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required:
|
||
* - email
|
||
* - password
|
||
* - email_code
|
||
* properties:
|
||
* email:
|
||
* type: string
|
||
* description: 邮箱地址
|
||
* example: 'user@example.com'
|
||
* password:
|
||
* type: string
|
||
* description: 密码
|
||
* example: 'password123'
|
||
* email_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 { email, password, email_code, invite_code } = body;
|
||
const { hashPassword, maskEmail } = require('../utils/crypto_utils');
|
||
|
||
// 验证必填参数
|
||
if (!email || !password || !email_code) {
|
||
return ctx.fail('邮箱、密码和验证码不能为空');
|
||
}
|
||
|
||
// 验证邮箱格式
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(email)) {
|
||
return ctx.fail('邮箱格式不正确');
|
||
}
|
||
|
||
// 验证密码长度
|
||
if (password.length < 6 || password.length > 50) {
|
||
return ctx.fail('密码长度必须在6-50位之间');
|
||
}
|
||
|
||
// 统一邮箱地址为小写
|
||
const email_normalized = email.toLowerCase().trim();
|
||
|
||
// 验证验证码
|
||
const emailVerifyResult = await email_service.verifyEmailCode(email_normalized, 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: email_normalized }
|
||
});
|
||
|
||
if (existingUser) {
|
||
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('邀请码无效,邀请人不存在');
|
||
}
|
||
}
|
||
|
||
// 生成设备SN码(基于邮箱和时间戳)
|
||
const sn_code = `SN${Date.now()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
||
|
||
// 加密密码
|
||
const hashedPassword = await hashPassword(password);
|
||
|
||
// 创建新用户(使用统一的小写邮箱和加密密码)
|
||
// 新用户注册赠送3天试用期
|
||
const newUser = await pla_account.create({
|
||
name: email_normalized.split('@')[0], // 默认使用邮箱用户名作为名称
|
||
sn_code: sn_code,
|
||
device_id: '', // 设备ID由客户端登录时提供
|
||
platform_type: 'boss', // 默认平台类型
|
||
login_name: email_normalized,
|
||
pwd: hashedPassword, // 使用加密后的密码
|
||
keyword: '',
|
||
is_enabled: 1,
|
||
is_delete: 0,
|
||
authorization_date: new Date(), // 新用户从注册日期开始
|
||
authorization_days: 3 // 赠送3天试用期
|
||
});
|
||
|
||
// 给邀请人增加3天试用期
|
||
if (inviter) {
|
||
const inviterData = inviter.toJSON();
|
||
const currentAuthDate = inviterData.authorization_date;
|
||
const currentAuthDays = inviterData.authorization_days || 0;
|
||
|
||
let newAuthDate;
|
||
let newAuthDays;
|
||
|
||
// 如果当前没有授权日期,则从今天开始
|
||
if (!currentAuthDate) {
|
||
newAuthDate = new Date();
|
||
newAuthDays = currentAuthDays + 3; // 累加3天
|
||
} else {
|
||
// 检查当前授权是否已过期
|
||
const currentEndDate = dayjs(currentAuthDate).add(currentAuthDays, 'day');
|
||
const now = dayjs();
|
||
|
||
if (currentEndDate.isBefore(now)) {
|
||
// 授权已过期,重新激活并给予3天试用期
|
||
newAuthDate = new Date();
|
||
newAuthDays = 3; // 过期后重新激活给3天
|
||
} else {
|
||
// 授权未过期,在现有基础上累加3天
|
||
newAuthDate = currentAuthDate;
|
||
newAuthDays = currentAuthDays + 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: email, // 使用邮箱代替手机号
|
||
invite_code: invite_code,
|
||
register_time: new Date(),
|
||
reward_status: 1, // 已发放
|
||
reward_type: 'trial_days',
|
||
reward_value: 3,
|
||
is_delete: 0
|
||
});
|
||
|
||
console.log(`[邀请注册] 用户 ${maskEmail(email_normalized)} 通过邀请码 ${invite_code} 注册成功,邀请人 ${inviter.sn_code} 获得3天试用期`);
|
||
} else {
|
||
console.log(`[邀请注册] 用户 ${maskEmail(email_normalized)} 注册成功(无邀请码)`);
|
||
}
|
||
|
||
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
|
||
* /api/invite/send_email_code:
|
||
* post:
|
||
* summary: 发送验证码
|
||
* description: 发送验证码到指定邮箱地址
|
||
* tags: [前端-推广邀请]
|
||
* requestBody:
|
||
* required: true
|
||
* content:
|
||
* application/json:
|
||
* schema:
|
||
* type: object
|
||
* required:
|
||
* - email
|
||
* properties:
|
||
* email:
|
||
* type: string
|
||
* description: 邮箱地址
|
||
* example: 'user@example.com'
|
||
* responses:
|
||
* 200:
|
||
* description: 发送成功
|
||
*/
|
||
'POST /invite/send_email_code': async (ctx) => {
|
||
try {
|
||
const body = ctx.getBody();
|
||
const { email } = body;
|
||
|
||
if (!email) {
|
||
return ctx.fail('邮箱地址不能为空');
|
||
}
|
||
|
||
// 验证邮箱格式
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(email)) {
|
||
return ctx.fail('邮箱格式不正确');
|
||
}
|
||
|
||
// 统一邮箱地址为小写
|
||
const email_normalized = email.toLowerCase().trim();
|
||
|
||
// 发送验证码
|
||
const emailResult = await email_service.sendEmailCode(email_normalized);
|
||
if (!emailResult.success) {
|
||
return ctx.fail(emailResult.message || '发送验证码失败');
|
||
}
|
||
|
||
return ctx.success({
|
||
message: '验证码已发送',
|
||
expire_time: emailResult.expire_time || 300 // 默认5分钟过期
|
||
});
|
||
} catch (error) {
|
||
console.error('发送验证码失败:', error);
|
||
return ctx.fail('发送验证码失败: ' + error.message);
|
||
}
|
||
}
|
||
};
|