邀请系统技术方案
1. 设计目标与定位
邀请系统用于记录用户之间的邀请关系,是后续邀请奖励、分销结算、拉新统计等功能的基础数据层。本系统以轻量、通用、可扩展为设计原则,仅记录邀请关系本身,奖励发放等业务逻辑通过事件 + 监听器的方式解耦。
1.1 设计原则
| 原则 | 说明 |
|---|---|
| 关系唯一 | 同一被邀请人只能绑定一个邀请人,绑定后不可变更 |
| 多态设计 | 邀请人与被邀请人均采用多态关联,支持未来扩展到 Admin 邀请、第三方账号邀请等场景 |
| 仅记事实 | 模型只记录邀请关系,不承载奖励、状态等业务字段,保持数据层纯粹 |
| 事件驱动 | 邀请关系建立后触发事件,奖励/统计等逻辑由监听器处理,便于扩展 |
2. 数据模型
2.1 数据表:invite_codes
邀请码表,每条记录代表一个主体(User、Admin 等)持有的唯一邀请码。邀请码通过多态关联归属主体,从原 user_extras.invite_code 字段抽取独立建模。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT UNSIGNED PK | 主键 |
code | VARCHAR(26) | 邀请码(大写 ULID,全局唯一) |
owner_id | BIGINT UNSIGNED | 所属主体 ID |
owner_type | VARCHAR(64) | 所属主体类型(多态类型别名,如 user、admin) |
created_at | TIMESTAMP NULL | 创建时间 |
updated_at | TIMESTAMP NULL | 更新时间 |
索引
| 索引名 | 字段 | 类型 | 说明 |
|---|---|---|---|
uniq_code | code | 唯一索引 | 邀请码全局唯一 |
uniq_owner | owner_id, owner_type | 唯一索引 | 一个主体仅持有一个邀请码 |
2.2 模型:InviteCode
命名空间:App\Models\Invite\InviteCode表名:invite_codes
关联
/**
* 所属主体(多态)
*/
public function owner(): MorphToModel 结构
- 使用
#[Table]、#[Fillable]等属性注解(对齐项目现有风格) code、owner_id、owner_type为 fillablecreating钩子在未显式指定code时自动生成大写 ULID;generateUniqueCode()应用层查重 + DB 唯一索引双重兜底- 用户创建时由
UserObserver自动为其创建邀请码;用户硬删除时一并清理
2.3 数据表:invite_records
邀请记录表,每条记录代表一个邀请关系。
| 字段 | 类型 | 说明 |
|---|---|---|
id | BIGINT UNSIGNED PK | 主键 |
inviter_id | BIGINT UNSIGNED | 邀请人 ID |
inviter_type | VARCHAR(64) | 邀请人类型(多态类型别名,如 user、admin) |
invitee_id | BIGINT UNSIGNED | 被邀请人 ID |
invitee_type | VARCHAR(64) | 被邀请人类型(多态类型别名) |
description | VARCHAR(255) | 描述/备注,如「通过邀请码注册」 |
created_at | TIMESTAMP NULL | 创建时间 |
索引
| 索引名 | 字段 | 类型 | 说明 |
|---|---|---|---|
idx_inviter | inviter_id, inviter_type, created_at | 普通索引 | 查询某邀请人的邀请列表 |
uniq_invitee | invitee_id, invitee_type | 唯一索引 | 同一被邀请人只能有一条邀请记录 |
2.4 模型:InviteRecord
命名空间:App\Models\Invite\InviteRecord表名:invite_records
关联
/**
* 邀请人(多态)
*/
public function inviter(): MorphTo
/**
* 被邀请人(多态)
*/
public function invitee(): MorphToModel 结构
- 使用
#[Table]、#[Fillable]等属性注解(对齐项目现有风格) inviter_id、inviter_type、invitee_id、invitee_type、description为 fillable- 无
updated_at(模型中设置const UPDATED_AT = null)
2.5 多态映射
在 config/morph_maps.php 中注册的类型均可作为邀请方/被邀请方,邀请码模型自身也注册了多态别名:
| 类型别名 | 模型类 | 典型场景 |
|---|---|---|
user | App\Models\User | 用户邀请用户(主流场景) |
admin | App\Models\Admin\Admin | 后台管理员手动添加邀请关系 |
invite_code | App\Models\Invite\InviteCode | 邀请码多态归属 |
invite_record | App\Models\Invite\InviteRecord | 邀请记录多态关联 |
3. 核心流程
3.1 邀请关系建立(注册时)
用户注册携带 invite_code
↓
校验邀请码是否存在(invite_codes.code)
↓
通过邀请码反查所属主体 owner(多态,即邀请人)
↓
创建 InviteRecord 记录(inviter=邀请人, invitee=新用户)
↓
触发 InviteRelationshipCreated 事件
↓
监听器处理奖励发放等后续逻辑幂等保障:依靠 uniq_invitee 唯一索引,同一用户重复触发也只会有一条邀请记录。
4. 事件系统
4.1 事件列表
| 事件类 | 触发场景 | 携带数据 |
|---|---|---|
Events\Invite\InviteRelationshipCreated | 邀请关系建立成功 | inviteRecord(邀请记录模型实例) |
4.2 与注册流程的协作
RegisterController 收到 invite_code
↓
触发 InviteRegistered 事件(同步,立即返回)
↓
InviteRegisteredListener(队列异步)
↓
调用 InviteService::createRelationshipByCode()
↓
创建 InviteRecord → 触发 InviteRelationshipCreated 事件设计意图:
InviteRegistered是业务入口事件,表示"有人用邀请码注册了",由注册流程触发InviteRegisteredListener实现ShouldQueue,走队列异步执行,不阻塞注册响应InviteRelationshipCreated是数据落地事件,表示"邀请关系已写入数据库",供奖励、任务等下游监听
5. API 设计
5.1 前台用户端 API
需 auth:sanctum 认证,响应为 Laravel 标准分页结构(data / links / meta)。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/invite/records | 我的邀请记录列表(分页,支持 per_page) |
| GET | /api/v1/invite/stats | 邀请统计(invitee_count、has_inviter) |
邀请码不单独提供接口,已包含在
UserResource的invite_code字段中。
邀请记录列表响应字段:
| 字段 | 说明 |
|---|---|
id | 记录 ID |
invitee.id / .name / .username / .avatar | 被邀请人信息 |
description | 描述/备注 |
created_at | 邀请时间 |
5.2 后台管理 API
需 auth:admin 认证 + 对应权限。
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | /admin/invite-records | invite-records.index | 邀请记录分页列表 |
| GET | /admin/invite-records/{id} | invite-records.show | 邀请记录详情 |
列表筛选参数(由 InviteRecordIndexRequest 校验,InviteRecord::scopeFilter 执行):
| 参数 | 类型 | 说明 |
|---|---|---|
inviter_type | string | 邀请人类型(user / admin) |
inviter_id | int | 邀请人 ID |
invitee_type | string | 被邀请人类型(user / admin) |
invitee_id | int | 被邀请人 ID |
description | string | 描述模糊匹配 |
created_start | date | 邀请时间起始 |
created_end | date | 邀请时间截止(须 ≥ created_start) |
per_page | int | 每页条数(1-100) |
响应字段:id、inviter_type、inviter_id、inviter_name、invitee_type、invitee_id、invitee_name、description、created_at
5.3 后台菜单与权限
菜单挂在「用户管理」目录下(user.invite-record,sort 4),权限由迁移文件自动生成:
| 权限 key | 说明 |
|---|---|
invite-records.index | 邀请记录列表(菜单权限) |
invite-records.show | 查看详情(按钮权限) |
6. 与现有系统的集成点
6.1 User 模型
通过 trait(如 HasInvitation)为 User、Admin 等模型追加:
// 我的邀请码(多态归属,morph 名 owner,一个主体一个码)
$this->inviteCode(): MorphOne
// 我邀请的人
$this->invitees(): MorphMany // 通过 InviteRecord 的 inviter 反向关联
// 我的邀请人
$this->inviterRelation(): MorphOne // 通过 InviteRecord 的 invitee 反向关联
// 快捷方法
$this->hasInviter(): bool
$this->getInviteCode(): string用户创建时 UserObserver 自动创建 InviteCode,用户硬删除时一并清理;UserExtra 不再承载邀请码。
6.2 注册流程
在 RegisterController 中,收到 invite_code 后:
- 表单校验:
Rule::exists(InviteCode::class, 'code')校验邀请码有效 - 新增逻辑:
InviteService通过邀请码反查 owner(邀请人),创建InviteRecord,触发InviteRelationshipCreated事件 - 邀请码无效、自邀或已存在邀请关系时静默跳过,不影响注册主流程
6.3 奖励系统(暂缓)
邀请奖励(金币/积分/VIP)后续可通过监听 InviteRelationshipCreated 事件处理,不写入邀请记录表本身。奖励类型枚举 CoinType::TYPE_INVITE_REGISTER 和 PointType::TYPE_INVITE_REGISTER 已预置,待需要时接入。
7. 实施阶段
| 阶段 | 内容 | 交付物 | 状态 |
|---|---|---|---|
| Phase 1 数据模型 | invite_codes / invite_records 表迁移 + InviteCode、InviteRecord 模型 + 多态映射 + 工厂 + 单元测试 | 邀请码与邀请关系数据层 | ✅ 已完成 |
| Phase 2 关系建立 | 用户创建自动发放邀请码 + 注册流程集成(创建邀请关系)+ InviteRelationshipCreated 事件 + 功能测试 | 邀请码自动生成、邀请关系可正常建立 | ✅ 已完成 |
| Phase 3 查询接口 | 前台「我的邀请列表/统计」+ 后台邀请记录列表与详情 + 筛选作用域 + 权限菜单 | 前后端查询能力完整 | ✅ 已完成 |
| Phase 4 文档与发布 | Apifox 全量发布 + README 更新 | 生产可用 | ✅ 已完成 |
暂缓项(后续按需启动):
邀请奖励发放(金币/积分/VIP 奖励、双向奖励)与用户任务系统联动(触发invite_user任务进度)- 测试覆盖率量化(需环境安装 Xdebug 或 PCOV 后执行
php artisan test --coverage --min=70)
7.1 Apifox 发布记录
项目:Laravel Skel(8720052),分支 main。
数据模型(Schema)
严格遵循模块隔离,Admin 与 Api 各自独立定义,禁止跨模块 $ref:
| Schema ID | 名称 | 模块 | 目录 |
|---|---|---|---|
312589523 | InviteRecord | Api (8396001) | 用户相关 (20995717) |
312590135 | InviteRecordAdminResource | Admin (8396002) | 用户相关 (20885420) |
接口
| 接口 ID | 名称 | 方法 | 路径 | 模块 | 目录 |
|---|---|---|---|---|---|
513246901 | 获取我的邀请记录 | GET | /api/v1/invite/records | Api | V1/邀请接口 (95504233) |
513246906 | 获取邀请统计 | GET | /api/v1/invite/stats | Api | V1/邀请接口 (95504233) |
513246910 | 获取邀请记录列表 | GET | /admin/invite-records | Admin | 用户管理/邀请记录 (95504235) |
513246915 | 获取邀请记录详情 | GET | /admin/invite-records/{inviteRecord} | Admin | 用户管理/邀请记录 (95504235) |
分页规范
两个列表接口均按项目规范启用全局参数 page / per_page,并通过 x-apifox-refs 引用对应模块的 EloquentPaginate 后覆写 data 字段:
| 模块 | EloquentPaginate ID |
|---|---|
| Api | 307396242 |
| Admin | 304922372 |
标签
- 前台:
邀请、分页接口(仅列表) - 后台:
邀请记录、分页接口(仅列表)
8. 代码结构
app/
├── Models/
│ └── Invite/
│ ├── InviteCode.php # 邀请码模型(多态归属 owner)
│ └── InviteRecord.php # 邀请记录模型(含 scopeFilter)
├── Events/
│ ├── Invite/
│ │ └── InviteRelationshipCreated.php # 数据落地事件
│ └── User/
│ └── InviteRegistered.php # 业务入口事件
├── Listeners/
│ └── User/
│ └── InviteRegisteredListener.php # 邀请注册监听器(队列异步)
├── Services/
│ └── InviteService.php # 邀请关系建立编排
├── Support/
│ └── Traits/
│ └── HasInvitation.php # 邀请关联 trait
├── Observers/
│ └── UserObserver.php # 用户创建时自动发放邀请码
├── Http/
│ ├── Controllers/
│ │ ├── Api/V1/InviteController.php # 前台:records / stats
│ │ └── Admin/InviteRecordController.php # 后台:index / show
│ ├── Requests/
│ │ └── Admin/Invite/
│ │ └── InviteRecordIndexRequest.php # 后台列表筛选校验
│ └── Resources/
│ ├── Api/V1/InviteRecordResource.php # 前台资源
│ └── Admin/InviteRecordResource.php # 后台资源
config/
└── morph_maps.php # invite_code / invite_record 别名
routes/
├── api_v1.php # /api/v1/invite/*
└── admin.php # /admin/invite-records/*
database/
├── migrations/
│ ├── 0001_01_01_000018_create_invite_codes_table.php
│ ├── 0001_01_01_000019_create_invite_records_table.php
│ └── 2026_10_31_111934_seed_initial_admin_data.php # 菜单与权限
└── factories/
└── Invite/
├── InviteCodeFactory.php
└── InviteRecordFactory.php
tests/
├── Unit/
│ ├── Models/Invite/
│ │ ├── InviteCodeTest.php
│ │ └── InviteRecordTest.php
│ ├── Models/User/HasInvitationTest.php
│ ├── Listeners/InviteRegisteredListenerTest.php
│ ├── Observers/UserObserverTest.php
│ └── Services/InviteServiceTest.php
└── Feature/
├── Api/V1/InviteControllerTest.php
└── Admin/InviteRecordControllerTest.php