feat: 迭代

This commit is contained in:
2026-05-21 19:01:49 +08:00
parent 606895fdd5
commit 083ba3e754
50 changed files with 4749 additions and 331 deletions
+778
View File
@@ -0,0 +1,778 @@
# 邀请码系统设计文档
> **版本**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