Skip to content

邀请系统技术方案 ​

1. 设计目标与定位 ​

邀请系统用于记录用户之间的邀请关系,是后续邀请奖励、分销结算、拉新统计等功能的基础数据层。本系统以轻量、通用、可扩展为设计原则,仅记录邀请关系本身,奖励发放等业务逻辑通过事件 + 监听器的方式解耦。

1.1 设计原则 ​

原则说明
关系唯一同一被邀请人只能绑定一个邀请人,绑定后不可变更
多态设计邀请人与被邀请人均采用多态关联,支持未来扩展到 Admin 邀请、第三方账号邀请等场景
仅记事实模型只记录邀请关系,不承载奖励、状态等业务字段,保持数据层纯粹
事件驱动邀请关系建立后触发事件,奖励/统计等逻辑由监听器处理,便于扩展

2. 数据模型 ​

2.1 数据表:invite_codes ​

邀请码表,每条记录代表一个主体(User、Admin 等)持有的唯一邀请码。邀请码通过多态关联归属主体,从原 user_extras.invite_code 字段抽取独立建模。

字段类型说明
idBIGINT UNSIGNED PK主键
codeVARCHAR(26)邀请码(大写 ULID,全局唯一)
owner_idBIGINT UNSIGNED所属主体 ID
owner_typeVARCHAR(64)所属主体类型(多态类型别名,如 user、admin)
created_atTIMESTAMP NULL创建时间
updated_atTIMESTAMP NULL更新时间

索引 ​

索引名字段类型说明
uniq_codecode唯一索引邀请码全局唯一
uniq_ownerowner_id, owner_type唯一索引一个主体仅持有一个邀请码

2.2 模型:InviteCode ​

命名空间:App\Models\Invite\InviteCode表名:invite_codes

关联 ​

php
/**
 * 所属主体(多态)
 */
public function owner(): MorphTo

Model 结构 ​

  • 使用 #[Table]、#[Fillable] 等属性注解(对齐项目现有风格)
  • code、owner_id、owner_type 为 fillable
  • creating 钩子在未显式指定 code 时自动生成大写 ULID;generateUniqueCode() 应用层查重 + DB 唯一索引双重兜底
  • 用户创建时由 UserObserver 自动为其创建邀请码;用户硬删除时一并清理

2.3 数据表:invite_records ​

邀请记录表,每条记录代表一个邀请关系。

字段类型说明
idBIGINT UNSIGNED PK主键
inviter_idBIGINT UNSIGNED邀请人 ID
inviter_typeVARCHAR(64)邀请人类型(多态类型别名,如 user、admin)
invitee_idBIGINT UNSIGNED被邀请人 ID
invitee_typeVARCHAR(64)被邀请人类型(多态类型别名)
descriptionVARCHAR(255)描述/备注,如「通过邀请码注册」
created_atTIMESTAMP NULL创建时间

索引 ​

索引名字段类型说明
idx_inviterinviter_id, inviter_type, created_at普通索引查询某邀请人的邀请列表
uniq_inviteeinvitee_id, invitee_type唯一索引同一被邀请人只能有一条邀请记录

2.4 模型:InviteRecord ​

命名空间:App\Models\Invite\InviteRecord表名:invite_records

关联 ​

php
/**
 * 邀请人(多态)
 */
public function inviter(): MorphTo

/**
 * 被邀请人(多态)
 */
public function invitee(): MorphTo

Model 结构 ​

  • 使用 #[Table]、#[Fillable] 等属性注解(对齐项目现有风格)
  • inviter_id、inviter_type、invitee_id、invitee_type、description 为 fillable
  • 无 updated_at(模型中设置 const UPDATED_AT = null)

2.5 多态映射 ​

在 config/morph_maps.php 中注册的类型均可作为邀请方/被邀请方,邀请码模型自身也注册了多态别名:

类型别名模型类典型场景
userApp\Models\User用户邀请用户(主流场景)
adminApp\Models\Admin\Admin后台管理员手动添加邀请关系
invite_codeApp\Models\Invite\InviteCode邀请码多态归属
invite_recordApp\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-recordsinvite-records.index邀请记录分页列表
GET/admin/invite-records/{id}invite-records.show邀请记录详情

列表筛选参数(由 InviteRecordIndexRequest 校验,InviteRecord::scopeFilter 执行):

参数类型说明
inviter_typestring邀请人类型(user / admin)
inviter_idint邀请人 ID
invitee_typestring被邀请人类型(user / admin)
invitee_idint被邀请人 ID
descriptionstring描述模糊匹配
created_startdate邀请时间起始
created_enddate邀请时间截止(须 ≥ created_start)
per_pageint每页条数(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 等模型追加:

php
// 我的邀请码(多态归属,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 后:

  1. 表单校验:Rule::exists(InviteCode::class, 'code') 校验邀请码有效
  2. 新增逻辑:InviteService 通过邀请码反查 owner(邀请人),创建 InviteRecord,触发 InviteRelationshipCreated 事件
  3. 邀请码无效、自邀或已存在邀请关系时静默跳过,不影响注册主流程

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名称模块目录
312589523InviteRecordApi (8396001)用户相关 (20995717)
312590135InviteRecordAdminResourceAdmin (8396002)用户相关 (20885420)

接口 ​

接口 ID名称方法路径模块目录
513246901获取我的邀请记录GET/api/v1/invite/recordsApiV1/邀请接口 (95504233)
513246906获取邀请统计GET/api/v1/invite/statsApiV1/邀请接口 (95504233)
513246910获取邀请记录列表GET/admin/invite-recordsAdmin用户管理/邀请记录 (95504235)
513246915获取邀请记录详情GET/admin/invite-records/{inviteRecord}Admin用户管理/邀请记录 (95504235)

分页规范 ​

两个列表接口均按项目规范启用全局参数 page / per_page,并通过 x-apifox-refs 引用对应模块的 EloquentPaginate 后覆写 data 字段:

模块EloquentPaginate ID
Api307396242
Admin304922372

标签 ​

  • 前台:邀请、分页接口(仅列表)
  • 后台:邀请记录、分页接口(仅列表)

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