Files
2026-05-21 19:01:49 +08:00

779 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 邀请码系统设计文档
> **版本**v1.0
> **最后更新**2026-05-13
> **状态**:✅ 已实现
---
## 📋 目录
1. [系统概述](#系统概述)
2. [核心功能](#核心功能)
3. [技术实现](#技术实现)
4. [数据库设计](#数据库设计)
5. [业务流程](#业务流程)
6. [API接口](#api接口)
7. [前端实现](#前端实现)
---
## 系统概述
### 功能定位
邀请码系统是一个用户增长工具,通过邀请返现机制激励用户推广平台,实现用户裂变增长。
### 核心特性
- ✅ 用户注册时自动生成唯一邀请码
- ✅ 支持小程序码扫码邀请
- ✅ 邀请关系自动绑定
- ✅ 防重复绑定和自我邀请
- ✅ 邀请统计和返现管理
---
## 核心功能
### 1. 邀请码生成规则
**格式规范**
- 长度:6位
- 字符集:数字 0-9 + 大写字母 A-Z(共36个字符)
- 示例:`A3X9K2`, `B7M4N1`, `5K8P2Z`
**生成时机**
- 用户注册时自动生成
- 微信授权登录(新用户)
- 微信手机号登录(新用户)
- 开发模式自动注册
**唯一性保证**
- 生成后检查数据库是否存在
- 如果重复则递归重新生成
- 理论上可生成 36^6 = 2,176,782,336 个不同的邀请码
### 2. 邀请关系绑定
**绑定时机**
- ✅ 用户注册时绑定
- ❌ 用户登录时不绑定(老用户)
**绑定规则**
1. 只有新注册用户才能绑定邀请关系
2. 每个用户只能被邀请一次(防重复绑定)
3. 用户不能使用自己的邀请码(防自我邀请)
4. 邀请码必须存在且有效
**绑定流程**
```
扫码进入 → 存储邀请码 → 用户注册 → 验证邀请码 → 创建邀请关系 → 更新统计
```
### 3. 邀请统计
**统计维度**
- `totalInvites`:累计邀请人数
- `totalOrders`:被邀请人累计订单数
- `totalCashback`:累计返现金额
- `availableBalance`:可用余额
- `withdrawnAmount`:已提现金额
**更新时机**
- 邀请关系创建时:`totalInvites +1`
- 被邀请人下单时:`totalOrders +1`, `totalCashback += 返现金额`
- 提现时:`availableBalance -= 提现金额`, `withdrawnAmount += 提现金额`
---
## 技术实现
### 后端实现(NestJS
#### 核心服务方法
**1. 生成邀请码**
```typescript
// apps/server/src/modules/app/auth/auth.service.ts
private generateInviteCode(): string {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let code = '';
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}
```
**2. 创建用户邀请码**
```typescript
private async createUserInviteCode(userId: number): Promise<void> {
// 1. 查找启用的邀请返现活动
const activity = await this.activityRepo
.createQueryBuilder('activity')
.where('activity.type = :type', { type: 'invite_cashback' })
.andWhere('activity.enabled = :enabled', { enabled: true })
.orderBy('activity.id', 'DESC')
.getOne();
if (!activity) {
this.logger.warn('没有启用的邀请返现活动,跳过生成邀请码');
return;
}
// 2. 生成邀请码并检查唯一性
const inviteCode = this.generateInviteCode();
const existing = await this.inviteStatsRepo.findOne({
where: { inviteCode },
});
if (existing) {
// 递归重新生成
return this.createUserInviteCode(userId);
}
// 3. 创建邀请统计记录
const inviteStats = this.inviteStatsRepo.create({
activityId: activity.id,
userId,
inviteCode,
totalInvites: 0,
totalOrders: 0,
totalCashback: 0,
availableBalance: 0,
withdrawnAmount: 0,
});
await this.inviteStatsRepo.save(inviteStats);
}
```
**3. 处理邀请关系绑定**
```typescript
private async handleInviteCode(inviteeId: number, inviteCode: string): Promise<void> {
if (!inviteCode) return;
// 1. 检查用户是否已被邀请过
const existingInvitation = await this.invitationRepo.findOne({
where: { inviteeId },
});
if (existingInvitation) {
this.logger.log(`用户已被邀请过: userId=${inviteeId}`);
return;
}
// 2. 查找邀请人
const inviterStats = await this.inviteStatsRepo.findOne({
where: { inviteCode },
});
if (!inviterStats) {
this.logger.warn(`邀请码不存在: ${inviteCode}`);
return;
}
// 3. 防止自我邀请
if (inviterStats.userId === inviteeId) {
this.logger.warn(`用户不能使用自己的邀请码: userId=${inviteeId}`);
return;
}
// 4. 创建邀请关系
const invitation = this.invitationRepo.create({
activityId: inviterStats.activityId,
inviterId: inviterStats.userId,
inviteeId,
inviteCode,
});
await this.invitationRepo.save(invitation);
// 5. 更新邀请人统计
inviterStats.totalInvites += 1;
await this.inviteStatsRepo.save(inviterStats);
this.logger.log(`邀请关系绑定成功: inviter=${inviterStats.userId}, invitee=${inviteeId}`);
}
```
**4. 注册流程集成**
```typescript
async register(dto: RegisterDto) {
// 1. 创建用户
const user = await this.userRepo.save(newUser);
// 2. 创建用户账户
await this.createUserAccount(user.id);
// 3. 生成用户邀请码
await this.createUserInviteCode(user.id);
// 4. 处理邀请关系绑定
if (dto.inviteCode) {
await this.handleInviteCode(user.id, dto.inviteCode);
}
// 5. 返回token
return this.generateToken(user);
}
```
#### 依赖注入配置
**AuthModule**
```typescript
// apps/server/src/modules/app/auth/auth.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([
User,
UserAccount,
MktInvitation,
MktUserInviteStats,
MktActivity,
]),
JwtModule.registerAsync({...}),
],
controllers: [AuthController],
providers: [AuthService],
exports: [AuthService, JwtModule],
})
export class UserAuthModule {}
```
---
## 数据库设计
### 1. 邀请关系表(mkt_invitations
```sql
CREATE TABLE `mkt_invitations` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`activity_id` int NOT NULL COMMENT '活动ID',
`inviter_id` int NOT NULL COMMENT '邀请人用户ID',
`invitee_id` int NOT NULL COMMENT '被邀请人用户ID',
`invite_code` varchar(20) NOT NULL COMMENT '邀请码',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_invitee` (`invitee_id`),
KEY `idx_inviter` (`inviter_id`),
KEY `idx_activity` (`activity_id`),
KEY `idx_invite_code` (`invite_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='邀请关系表';
```
**字段说明**
- `inviter_id`:邀请人ID
- `invitee_id`:被邀请人ID(唯一索引,保证一个用户只能被邀请一次)
- `invite_code`:使用的邀请码
### 2. 用户邀请统计表(mkt_user_invite_stats
```sql
CREATE TABLE `mkt_user_invite_stats` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`activity_id` int NOT NULL COMMENT '活动ID',
`user_id` int NOT NULL COMMENT '用户ID',
`invite_code` varchar(20) NOT NULL COMMENT '邀请码',
`total_invites` int NOT NULL DEFAULT '0' COMMENT '累计邀请人数',
`total_orders` int NOT NULL DEFAULT '0' COMMENT '被邀请人累计订单数',
`total_cashback` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '累计返现金额',
`available_balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '可用余额',
`withdrawn_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已提现金额',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_activity` (`user_id`, `activity_id`),
UNIQUE KEY `uk_invite_code` (`invite_code`),
KEY `idx_activity` (`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户邀请统计表';
```
**字段说明**
- `invite_code`:用户的邀请码(唯一索引)
- `total_invites`:累计邀请人数
- `total_cashback`:累计返现金额
- `available_balance`:可用余额(可提现)
- `withdrawn_amount`:已提现金额
### 3. 营销活动表(mkt_activities
```sql
CREATE TABLE `mkt_activities` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`type` varchar(50) NOT NULL COMMENT '活动类型:invite_cashback-邀请返现',
`name` varchar(100) NOT NULL COMMENT '活动名称',
`description` text COMMENT '活动描述',
`config` json COMMENT '活动配置(JSON格式)',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`start_time` timestamp NULL COMMENT '开始时间',
`end_time` timestamp NULL COMMENT '结束时间',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_type` (`type`),
KEY `idx_enabled` (`enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营销活动表';
```
**活动配置示例**
```json
{
"firstOrderRate": 0.05, // 首单返现比例 5%
"normalOrderRate": 0.03, // 持续订单返现比例 3%
"minOrderAmount": 100, // 最低订单金额
"maxCashback": 500 // 单笔最高返现
}
```
---
## 业务流程
### 完整邀请流程
```
┌─────────────┐
│ 用户A注册 │
└──────┬──────┘
┌─────────────────────┐
│ 生成邀请码: A3X9K2 │
└──────┬──────────────┘
┌─────────────────────┐
│ 创建邀请统计记录 │
└─────────────────────┘
┌─────────────────────┐
│ 用户B扫描A的小程序码│
└──────┬──────────────┘
┌─────────────────────┐
│ 小程序首页获取scene │
│ 参数: i=A3X9K2 │
└──────┬──────────────┘
┌─────────────────────┐
│ 存储到本地Storage │
│ pendingInviteCode │
└─────────────────────┘
┌─────────────────────┐
│ 用户B注册账号 │
└──────┬──────────────┘
┌─────────────────────┐
│ 生成B的邀请码: │
│ B7M4N1 │
└──────┬──────────────┘
┌─────────────────────┐
│ 读取本地邀请码 │
│ A3X9K2 │
└──────┬──────────────┘
┌─────────────────────┐
│ 验证邀请码有效性 │
│ - 邀请码存在 │
│ - 非自我邀请 │
│ - 未被邀请过 │
└──────┬──────────────┘
┌─────────────────────┐
│ 创建邀请关系记录 │
│ inviter: A │
│ invitee: B │
└──────┬──────────────┘
┌─────────────────────┐
│ 更新A的邀请统计 │
│ totalInvites +1 │
└──────┬──────────────┘
┌─────────────────────┐
│ 清除本地邀请码 │
└─────────────────────┘
┌─────────────────────┐
│ 用户B下单支付 │
└──────┬──────────────┘
┌─────────────────────┐
│ 计算返现金额 │
│ - 首单: 5% │
│ - 持续: 3% │
└──────┬──────────────┘
┌─────────────────────┐
│ 更新A的邀请统计 │
│ - totalOrders +1 │
│ - totalCashback += │
│ - availableBalance +=│
└─────────────────────┘
```
---
## API接口
### 1. 用户注册(带邀请码)
**接口**`POST /api/app/auth/register`
**请求参数**
```typescript
{
phone: string; // 手机号
code: string; // 验证码
password?: string; // 密码(可选)
nickname?: string; // 昵称(可选)
inviteCode?: string; // 邀请码(可选)
}
```
**响应**
```typescript
{
token: string; // JWT token
user: {
id: number;
phone: string;
nickname: string;
// ...
}
}
```
### 2. 微信授权登录(带邀请码)
**接口**`POST /api/app/auth/login/wechat`
**请求参数**
```typescript
{
code: string; // 微信授权code
nickname?: string; // 昵称
avatar?: string; // 头像
inviteCode?: string; // 邀请码(新用户)
}
```
### 3. 获取用户邀请信息
**接口**`GET /api/app/invite/stats`
**响应**
```typescript
{
inviteCode: string; // 我的邀请码
totalInvites: number; // 累计邀请人数
totalOrders: number; // 被邀请人订单数
totalCashback: number; // 累计返现
availableBalance: number; // 可用余额
withdrawnAmount: number; // 已提现金额
}
```
### 4. 获取邀请列表
**接口**`GET /api/app/invite/list`
**响应**
```typescript
{
list: [
{
inviteeId: number; // 被邀请人ID
inviteeName: string; // 被邀请人昵称
inviteeAvatar: string; // 被邀请人头像
orderCount: number; // 订单数
cashbackAmount: number; // 返现金额
createdAt: string; // 邀请时间
}
],
total: number;
}
```
---
## 前端实现
### 小程序端(uni-app
#### 1. 首页获取邀请码
**文件**`apps/miniapp/src/pages/index/index.vue`
```typescript
import { onLoad } from '@dcloudio/uni-app';
onLoad((options) => {
// 处理小程序码 scene 参数
if (options.scene) {
try {
const decodedScene = decodeURIComponent(options.scene);
const params = new URLSearchParams(decodedScene);
const inviteCode = params.get('i');
if (inviteCode) {
// 存储邀请码到本地
uni.setStorageSync('pendingInviteCode', inviteCode);
console.log('保存邀请码:', inviteCode);
}
} catch (error) {
console.error('解析scene参数失败:', error);
}
}
});
```
#### 2. 注册时传递邀请码
**文件**`apps/miniapp/src/pages/register/index.vue`
```typescript
const handleRegister = async () => {
// 读取本地存储的邀请码
const inviteCode = uni.getStorageSync('pendingInviteCode');
// 调用注册接口
const res = await register({
phone: form.phone,
code: form.code,
password: form.password,
inviteCode, // 传递邀请码
});
// 注册成功后清除邀请码
if (inviteCode) {
uni.removeStorageSync('pendingInviteCode');
}
// 保存token并跳转
uni.setStorageSync('token', res.token);
uni.switchTab({ url: '/pages/index/index' });
};
```
#### 3. 邀请海报页面
**文件**`apps/miniapp/src/pages/invite/poster.vue`
**功能**
- 显示用户邀请码
- 生成小程序码(二维码)
- 展示返现规则
- 保存海报图片
- 复制邀请码
**关键代码**
```vue
<template>
<view class="page-poster">
<!-- 海报卡片 -->
<view class="poster-card">
<!-- 顶部英雄区 -->
<view class="poster-hero">
<text class="hero-title">邀请好友 赚现金</text>
<text class="hero-subtitle">好友下单您赚钱</text>
</view>
<!-- 收益展示 -->
<view class="earnings-section">
<view class="earning-item">
<text class="earning-value">5%</text>
<text class="earning-label">首单返现</text>
</view>
<view class="earning-item">
<text class="earning-value">3%</text>
<text class="earning-label">持续返现</text>
</view>
</view>
<!-- 二维码区域 -->
<view class="qrcode-section">
<canvas canvas-id="qrcode" class="qrcode-canvas"></canvas>
<text class="qrcode-tip">长按识别二维码</text>
</view>
<!-- 邀请码 -->
<view class="invite-code-section">
<text class="invite-code-label">我的邀请码</text>
<text class="invite-code-value">{{ inviteCode }}</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-bar">
<button class="btn-primary" @tap="saveImage">保存海报</button>
<button class="btn-secondary" @tap="copyInviteCode">复制邀请码</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { getInviteStats } from '@/api/user/invite';
const inviteCode = ref('');
onMounted(async () => {
// 获取用户邀请信息
const res = await getInviteStats();
inviteCode.value = res.inviteCode;
// 生成小程序码
generateQRCode();
});
const generateQRCode = () => {
// 使用 canvas 生成小程序码
// scene 参数格式: i=邀请码
const scene = `i=${inviteCode.value}`;
// ... 生成二维码逻辑
};
const copyInviteCode = () => {
uni.setClipboardData({
data: inviteCode.value,
success: () => {
uni.showToast({ title: '邀请码已复制', icon: 'success' });
},
});
};
</script>
```
#### 4. API封装
**文件**`apps/miniapp/src/api/user/auth.ts`
```typescript
// 注册
export function register(data: {
phone: string;
code: string;
password?: string;
nickname?: string;
inviteCode?: string;
}) {
return post('/api/app/auth/register', data);
}
// 微信授权登录
export function loginByWechat(
code: string,
nickname?: string,
avatar?: string,
inviteCode?: string
) {
return post('/api/app/auth/login/wechat', {
code,
nickname,
avatar,
inviteCode,
});
}
```
**文件**`apps/miniapp/src/api/user/invite.ts`
```typescript
// 获取邀请统计
export function getInviteStats() {
return get('/api/app/invite/stats');
}
// 获取邀请列表
export function getInviteList(params: {
page: number;
pageSize: number;
}) {
return get('/api/app/invite/list', params);
}
```
---
## 测试场景
### 功能测试
#### 1. 邀请码生成测试
- [ ] 用户注册后自动生成邀请码
- [ ] 邀请码格式正确(6位,0-9A-Z)
- [ ] 邀请码唯一性(不重复)
- [ ] 微信授权登录新用户生成邀请码
#### 2. 邀请关系绑定测试
- [ ] 扫码进入小程序,邀请码正确存储
- [ ] 注册时邀请码正确传递
- [ ] 邀请关系正确创建
- [ ] 邀请人统计正确更新(totalInvites +1
- [ ] 防重复绑定(已被邀请的用户不能再次绑定)
- [ ] 防自我邀请(不能使用自己的邀请码)
- [ ] 无效邀请码处理(邀请码不存在)
#### 3. 邀请统计测试
- [ ] 邀请人数统计正确
- [ ] 订单数统计正确
- [ ] 返现金额计算正确
- [ ] 可用余额更新正确
#### 4. 边界情况测试
- [ ] 没有启用的邀请活动时的处理
- [ ] 邀请码生成冲突时的重试机制
- [ ] 并发注册时的邀请码唯一性
- [ ] 老用户登录时不处理邀请码
---
## 注意事项
### 安全性
1. **邀请码唯一性**:必须确保邀请码在数据库中唯一
2. **防刷机制**:需要防止恶意刷邀请关系
3. **数据一致性**:邀请统计数据需要与实际订单数据保持一致
### 性能优化
1. **邀请码索引**:在 `invite_code` 字段上建立唯一索引
2. **查询优化**:邀请列表查询需要分页和索引优化
3. **缓存策略**:用户邀请统计可以使用 Redis 缓存
### 扩展性
1. **多活动支持**:系统支持多个邀请活动同时进行
2. **返现规则配置**:返现比例可在活动配置中灵活调整
3. **邀请码格式**:可以根据需要调整邀请码长度和字符集
---
## 更新日志
| 日期 | 版本 | 说明 |
|------|------|------|
| 2026-05-13 | v1.0 | 初始版本,完成邀请码系统设计和实现 |
---
**维护团队**:开发团队
**最后更新**2026-05-13