From 98d9abeae58159b6bf853310c96a0abfc1e51109 Mon Sep 17 00:00:00 2001 From: pqcqaq <905739777@qq.com> Date: Sun, 26 Oct 2025 11:04:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=84=E7=94=9F=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/genernal-business/parasite.md | 1569 +++++++++++++++++++++++++++++ 1 file changed, 1569 insertions(+) create mode 100644 src/genernal-business/parasite.md diff --git a/src/genernal-business/parasite.md b/src/genernal-business/parasite.md new file mode 100644 index 0000000..94920f4 --- /dev/null +++ b/src/genernal-business/parasite.md @@ -0,0 +1,1569 @@ +# 寄生模式功能文档 + +## 1. 功能概述 + +### 1.1 核心价值 + +寄生模式(Parasite Mode)是 oak-general-business 提供的一种临时用户身份借用机制。它允许已注册用户生成专属的邀请链接或二维码,未注册的"影子用户"(shadow user)可以通过该链接临时登录并访问特定页面,无需完成完整的注册流程。 + +### 1.2 应用场景 + +**场景一:活动邀请** +- 某个活动需要用户邀请好友参与 +- 已注册用户生成专属邀请链接,包含特定的活动页面地址 +- 好友点击链接后,系统自动为其创建影子账号并登录,直接跳转到活动页面 +- 好友可以立即参与活动,无需先注册 + +**场景二:内容分享** +- 用户想分享某个需要登录才能查看的内容 +- 生成寄生链接,设置跳转到内容详情页 +- 接收者点击后自动以临时身份登录,直接查看内容 +- 可以设置链接的有效期和是否可重复使用 + +**场景三:表单填写** +- 用户需要收集多人的信息(如团队成员资料) +- 为每个成员生成独立的寄生链接 +- 成员点击链接后自动登录,填写表单 +- 系统可以追踪每个人的填写状态 + +### 1.3 核心特性 + +1. **临时身份借用**:影子用户通过寄生链接获得临时登录权限,无需注册 +2. **自动令牌生成**:点击链接后自动创建 Token,用户无感知登录 +3. **页面定向跳转**:可以指定登录后跳转到特定页面,并传递参数和状态 +4. **有效期控制**:支持设置链接过期时间,过期后无法使用 +5. **重复使用控制**:可以设置链接是否允许多次使用 +6. **自动作废机制**:当影子用户激活为正式用户后,所有相关的寄生记录自动失效 +7. **二维码支持**:可以生成二维码形式,方便线下场景使用 +8. **关联实体**:可以将寄生记录与特定实体(如活动、订单)关联,方便管理 + +### 1.4 与其他功能的关系 + +```mermaid +graph TB + User[用户系统] -->|创建影子用户| Parasite[寄生模式] + Parasite -->|生成临时令牌| Token[令牌管理] + Parasite -->|关联实体| Entity[业务实体] + Parasite -->|二维码| QrCode[二维码功能] + User -->|激活用户| DeactivateParasite[作废寄生记录] + Token -->|令牌过期| ExpiredParasite[同步寄生状态] +``` + +### 1.5 工作原理简述 + +1. **创建阶段**:业务方创建 Parasite 记录,指定影子用户、过期时间、跳转页面等信息 +2. **链接生成**:系统生成唯一的寄生链接(包含 Parasite ID) +3. **访问阶段**:用户点击链接,前端组件解析 ID 并调用 `wakeupParasite` 接口 +4. **令牌生成**:后端验证寄生记录有效性,为影子用户创建 Token +5. **自动登录**:前端存储 Token,完成自动登录 +6. **页面跳转**:根据配置的 `redirectTo` 跳转到目标页面 +7. **失效管理**:当设置为单次使用时,使用后立即标记为已过期;当用户激活时,所有相关寄生记录作废 + +--- + +**参考代码路径**: +- 实体定义:`src/entities/Parasite.ts` +- 业务逻辑:`src/aspects/token.ts` (wakeupParasite 方法) +- 触发器:`src/triggers/parasite.ts`, `src/triggers/user.ts` +- 前端组件:`src/components/parasite/` +- Feature:`src/features/token.ts` (wakeupParasite 方法) + +--- + +## 2. 实体定义详解 + +### 2.1 Parasite 实体 + +#### 2.1.1 Schema 定义 + +```typescript +interface Schema extends EntityShape { + user: User; // 关联的影子用户 + entity: String<32>; // 关联的实体类型(如 'activity', 'order' 等) + entityId: String<64>; // 关联的实体 ID + showTip?: Boolean; // 是否显示提示信息 + expiresAt: Datetime; // 过期时间 + expired: Boolean; // 是否已过期 + redirectTo: RedirectTo; // 跳转配置 + multiple?: Boolean; // 是否允许重复使用 + tokenLifeLength?: Int<4>; // 生成的 Token 的生命周期(毫秒) + tokens: Token[]; // 关联的令牌列表 +} + +type RedirectTo = { + pathname: string; // 路由路径 + props?: Record; // 页面参数 + state?: Record; // 页面状态 +}; +``` + +#### 2.1.2 字段详解 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `user` | User | 是 | 关联的影子用户,必须是 `userState` 为 `'shadow'` 的用户 | +| `entity` | String<32> | 是 | 关联的业务实体类型,用于标识寄生链接的使用场景 | +| `entityId` | String<64> | 是 | 关联的业务实体 ID,与 entity 配合使用 | +| `showTip` | Boolean | 否 | 控制是否在页面显示提示信息,如"您正在使用临时身份" | +| `expiresAt` | Datetime | 是 | 链接过期时间,过期后无法使用 | +| `expired` | Boolean | 是 | 标记链接是否已过期或作废 | +| `redirectTo` | RedirectTo | 是 | 登录成功后的跳转配置,包含路径、参数和状态 | +| `multiple` | Boolean | 否 | 是否允许重复使用。`false` 时使用一次后自动失效,`true` 时可以在有效期内重复使用 | +| `tokenLifeLength` | Int<4> | 否 | 生成的 Token 的有效期(毫秒)。如果不设置,使用系统默认配置 | +| `tokens` | Token[] | - | 通过此寄生链接生成的所有令牌(关联字段) | + +#### 2.1.3 Action(动作) + +Parasite 实体支持以下动作: + +| Action | 中文名称 | 说明 | +|--------|----------|------| +| `wakeup` | 激活 | 唤醒寄生链接,生成令牌。通常在链接被点击时触发 | +| `cancel` | 作废 | 手动作废寄生链接,使其失效 | +| `qrcode` | 采集码 | 生成寄生链接的二维码 | + +#### 2.1.4 实体关系图 + +```mermaid +erDiagram + Parasite ||--|| User : "关联影子用户" + Parasite ||--o{ Token : "生成令牌" + Parasite }o--|| Entity : "关联业务实体" + + Parasite { + string id PK + string userId FK + string entity + string entityId + boolean expired + datetime expiresAt + object redirectTo + boolean multiple + int tokenLifeLength + } + + User { + string id PK + string userState "必须为shadow" + string nickname + } + + Token { + string id PK + string entity "值为parasite" + string entityId FK + string userId FK + datetime disablesAt + } +``` + +### 2.2 RedirectTo 类型 + +RedirectTo 用于配置用户登录后的跳转行为: + +```typescript +type RedirectTo = { + pathname: string; // 目标页面路径,如 '/activity/detail' + props?: Record; // URL 查询参数,如 { id: '123', from: 'share' } + state?: Record; // 页面状态,通过路由状态传递,不会显示在 URL 中 +}; +``` + +**使用示例**: + +```typescript +// 跳转到活动详情页 +redirectTo: { + pathname: '/activity/detail', + props: { id: 'activity123' }, + state: { fromParasite: true } +} + +// 最终生成的 URL: /activity/detail?id=activity123 +// 页面可以通过 router.state 获取 { fromParasite: true } +``` + +### 2.3 数据约束和验证 + +1. **用户状态验证** + - 关联的 `user` 必须是影子用户(`userState === 'shadow'`) + - 如果用户已激活(`userState === 'normal'`),无法创建或使用寄生链接 + - 触发器会在用户激活时自动作废所有相关的寄生记录 + +2. **过期时间验证** + - `expiresAt` 必须是未来的时间 + - 系统会在调用 `wakeupParasite` 时检查是否过期 + - 过期的寄生链接无法生成令牌 + +3. **重复使用控制** + - 当 `multiple === false` 时,使用一次后 `expired` 自动设置为 `true` + - 当 `multiple === true` 时,可以在有效期内重复使用 + +4. **令牌生命周期** + - 如果设置了 `tokenLifeLength`,生成的 Token 的 `disablesAt` 为当前时间 + `tokenLifeLength` + - 如果未设置,使用系统的默认令牌有效期 + +--- + +**参考代码路径**: +- 实体定义:`src/entities/Parasite.ts` +- 相关实体:`src/entities/User.ts`, `src/entities/Token.ts` + +--- + +## 3. 业务逻辑说明 + +### 3.1 核心业务流程 + +#### 3.1.1 寄生链接使用完整流程 + +```mermaid +sequenceDiagram + participant User as 用户 + participant Frontend as 前端页面 + participant Excess as Excess组件 + participant Feature as Token Feature + participant Aspect as wakeupParasite Aspect + participant DB as 数据库 + + User->>Frontend: 点击寄生链接 + Frontend->>Excess: 加载页面,解析 oakId + Excess->>Feature: 调用 cache.refresh 查询 Parasite + Feature->>DB: SELECT * FROM parasite WHERE id=? + DB-->>Feature: 返回 Parasite 数据 + + alt Parasite 不存在 + Feature-->>Excess: 返回空数据 + Excess->>User: 显示"链接不合法" + else Parasite 已过期 + Feature-->>Excess: expired=true + Excess->>User: 显示"链接已过期" + else Parasite 有效 + Excess->>Feature: 移除旧 Token + Feature->>Feature: removeToken() + Excess->>Feature: 调用 wakeupParasite(oakId) + Feature->>Aspect: 调用 wakeupParasite aspect + + Aspect->>DB: 查询 Parasite 和关联的 User + DB-->>Aspect: 返回 Parasite 和 User + + alt User 已激活 + Aspect-->>Feature: 抛出异常"用户已登录" + Feature-->>Excess: 错误提示 + Excess->>User: 显示错误信息 + else User 是影子用户 + alt multiple=false (单次使用) + Aspect->>DB: UPDATE parasite SET expired=true + end + + Aspect->>DB: INSERT INTO token + DB-->>Aspect: Token 创建成功 + Aspect-->>Feature: 返回 tokenValue + Feature->>Feature: 保存 Token 到本地存储 + Feature-->>Excess: Token 生成成功 + + Excess->>Frontend: redirectTo(目标页面) + Frontend->>User: 显示目标页面(已登录状态) + end + end +``` + +#### 3.1.2 用户激活时的级联作废流程 + +```mermaid +sequenceDiagram + participant Admin as 管理员/系统 + participant Aspect as User Aspect + participant Trigger as User Trigger + participant DB as 数据库 + + Admin->>Aspect: 调用 user.activate() + Aspect->>DB: UPDATE user SET userState='normal' + DB-->>Trigger: 触发 afterUpdate 事件 + + Trigger->>DB: 查询该用户的所有未过期 Parasite + DB-->>Trigger: 返回 Parasite 列表 + + alt 存在未过期的 Parasite + Trigger->>DB: UPDATE parasite SET expired=true + Trigger->>DB: UPDATE token SET ableState='disabled'
WHERE entity='parasite' AND entityId IN (...) + DB-->>Trigger: 更新成功 + end + + Trigger-->>Aspect: 触发器执行完成 + Aspect-->>Admin: 用户激活成功 +``` + +### 3.2 Aspect 方法详解 + +#### 3.2.1 wakeupParasite + +**方法签名**: +```typescript +async function wakeupParasite( + params: { + id: string; // Parasite ID + env: WebEnv | WechatMpEnv | NativeEnv; // 登录环境 + }, + context: BRC +): Promise // 返回 Token 值 +``` + +**业务逻辑**: + +1. **查询 Parasite 记录** + ```typescript + const [parasite] = await context.select('parasite', { + data: { + id: 1, + expired: 1, + multiple: 1, + userId: 1, + tokenLifeLength: 1, + user: { id: 1, userState: 1 } + }, + filter: { id } + }); + ``` + +2. **验证有效性** + - 检查 `parasite.expired`,如果已过期抛出 `OakRowInconsistencyException` + - 检查 `parasite.user.userState`,必须为 `'shadow'`,否则抛出 `OakUserException` + +3. **处理单次使用模式** + ```typescript + if (!parasite.multiple) { + await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'wakeup', + data: { expired: true }, + filter: { id } + }); + } + ``` + +4. **生成 Token** + ```typescript + const tokenValue = await generateNewIdAsync(); + await context.operate('token', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + entity: 'parasite', + entityId: id, + userId: parasite.userId, + playerId: parasite.userId, + disablesAt: Date.now() + parasite.tokenLifeLength!, + env, + refreshedAt: Date.now(), + value: tokenValue, + applicationId: context.getApplicationId() + } + }); + ``` + +5. **加载 Token 信息并返回** + ```typescript + await loadTokenInfo(tokenValue, context); + return tokenValue; + ``` + +**异常处理**: +- `OakRowInconsistencyException`: 数据已过期 +- `OakUserException`: 用户已登录过系统,不允许借用身份 + +### 3.3 Trigger 详解 + +#### 3.3.1 Parasite 触发器 + +**触发器 1:过期时作废 Token** + +```typescript +{ + name: '当parasite过期时,使其相关token也过期', + entity: 'parasite', + action: 'update', + check: (operation) => !!operation.data.expired, + when: 'before', + fn: async ({ operation }, context) => { + await context.operate('token', { + id: await generateNewIdAsync(), + action: 'disable', + data: {}, + filter: { parasite: operation.filter } + }); + } +} +``` + +**触发器 2:取消时作废 Token** + +```typescript +{ + name: '当parasite失效时,使其相关token也过期', + entity: 'parasite', + action: 'cancel', + when: 'before', + fn: async ({ operation }, context) => { + await context.operate('token', { + id: await generateNewIdAsync(), + action: 'disable', + data: {}, + filter: { parasite: operation.filter } + }); + } +} +``` + +#### 3.3.2 User 触发器 + +**用户激活时作废所有 Parasite** + +```typescript +{ + name: '当用户被激活时,将所有的parasite作废', + entity: 'user', + action: 'activate', + when: 'after', + fn: async ({ operation }, context) => { + // 1. 查询该用户的所有未过期 Parasite + const parasiteList = await context.select('parasite', { + data: { id: 1 }, + filter: { + user: operation.filter, + expired: false + } + }); + + const parasiteIds = parasiteList.map(ele => ele.id!); + + if (parasiteIds.length > 0) { + // 2. 作废所有 Parasite + await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'update', + data: { expired: true }, + filter: { id: { $in: parasiteIds } } + }, { blockTrigger: true }); + + // 3. 禁用相关的 Token + await context.operate('token', { + id: await generateNewIdAsync(), + action: 'disable', + data: {}, + filter: { + ableState: 'enabled', + entity: 'parasite', + entityId: { $in: parasiteIds as string[] } + } + }, { blockTrigger: true }); + } + } +} +``` + +### 3.4 业务规则总结 + +1. **影子用户限制**:只有 `userState === 'shadow'` 的用户才能关联 Parasite +2. **过期检查**:每次使用前检查 `expired` 和 `expiresAt` +3. **单次使用模式**:`multiple === false` 时,使用后立即设置 `expired = true` +4. **用户激活作废**:用户从影子状态激活后,所有相关 Parasite 自动失效 +5. **Token 同步**:Parasite 过期或作废时,相关 Token 同步禁用 +6. **权限控制**:使用 `context.openRootMode()` 确保操作权限 + +--- + +**参考代码路径**: +- Aspect 实现:`src/aspects/token.ts` (第 3091-3176 行) +- Parasite 触发器:`src/triggers/parasite.ts` +- User 触发器:`src/triggers/user.ts` (第 128-185 行) + +--- + +## 4. 前端组件使用 + +### 4.1 组件列表 + +寄生模式提供以下前端组件: + +| 组件名称 | 路径 | 用途 | +|---------|------|------| +| ParasiteList | `components/parasite/list` | 寄生链接列表,用于管理和查看所有寄生记录 | +| ParasiteDetail | `components/parasite/detail` | 寄生链接详情,显示链接信息和二维码 | +| ParasiteUpsert | `components/parasite/upsert` | 创建/编辑寄生链接 | +| ParasiteExcess | `components/parasite/excess` | 寄生链接入口页面,处理链接点击和自动登录 | + +### 4.2 ParasiteList 组件 + +#### 4.2.1 组件说明 + +用于展示某个业务实体关联的所有寄生链接列表,支持作废和生成二维码操作。 + +#### 4.2.2 Props 定义 + +```typescript +interface Props { + entity: string; // 关联的实体类型 + entityId: string; // 关联的实体 ID + nameLabel: string; // 列表标题 +} +``` + +#### 4.2.3 使用示例 + +```typescript +import ParasiteList from 'oak-general-business/es/components/parasite/list'; + +// 在活动详情页中显示该活动的所有寄生链接 + +``` + +#### 4.2.4 功能特性 + +- 自动加载指定实体的所有寄生记录 +- 按创建时间倒序排列 +- 支持 `cancel`(作废)和 `qrcode`(生成二维码)操作 +- 显示用户昵称、过期状态、过期时间等信息 + +### 4.3 ParasiteDetail 组件 + +#### 4.3.1 组件说明 + +展示单个寄生链接的详细信息,包括链接地址和二维码。 + +#### 4.3.2 Props 定义 + +```typescript +interface Props { + oakId: string; // Parasite ID + disableDownload?: boolean; // 是否禁用二维码下载,默认 false + size?: number; // 二维码尺寸,默认 280 + disabled?: boolean; // 是否禁用,默认 false + color?: string; // 二维码前景色,默认 '#000000' + bgColor?: string; // 二维码背景色,默认 '#ffffff' +} +``` + +#### 4.3.3 使用示例 + +```typescript +import ParasiteDetail from 'oak-general-business/es/components/parasite/detail'; + +// 显示寄生链接详情和二维码 + +``` + +#### 4.3.4 功能特性 + +- 生成寄生链接 URL(格式:`https://domain.com/parasite/excess?oakId=xxx`) +- 显示二维码(仅限非小程序环境) +- 支持一键复制链接 +- 显示过期状态和过期时间 + +### 4.4 ParasiteUpsert 组件 + +#### 4.4.1 组件说明 + +创建或编辑寄生链接的表单组件,支持搜索和选择影子用户。 + +#### 4.4.2 Props 定义 + +```typescript +interface Props { + entity: keyof EntityDict; // 关联的实体类型 + entityId: string; // 关联的实体 ID + relation: string; // 用户关系名称(用于搜索影子用户) + redirectTo?: { // 跳转配置 + pathname: string; + props?: Record; + state?: Record; + }; + multiple?: boolean; // 是否允许重复使用,默认 false + nameLabel?: string; // 表单标题 + nameRequired?: boolean; // 用户是否必填,默认 true +} +``` + +#### 4.4.3 使用示例 + +```typescript +import ParasiteUpsert from 'oak-general-business/es/components/parasite/upsert'; + +// 为活动创建寄生链接 + +``` + +#### 4.4.4 功能特性 + +- 搜索影子用户(根据昵称模糊查询) +- 只显示与实体有特定关系的影子用户 +- 设置有效期(默认7天) +- 自动计算过期时间 +- 防抖搜索(500ms) + +#### 4.4.5 关键方法 + +**onSearch(value: string)** +- 搜索用户昵称,支持防抖 +- 过滤条件:`userState='shadow'` 且与实体有指定关系 + +**onSelect(value: string)** +- 选择用户后设置 `userId` + +### 4.5 ParasiteExcess 组件 + +#### 4.5.1 组件说明 + +寄生链接的入口页面,负责处理用户点击链接后的自动登录和页面跳转。这是整个寄生模式最核心的组件。 + +#### 4.5.2 Props 定义 + +```typescript +interface Props { + oakId: string; // 从 URL 查询参数获取的 Parasite ID +} +``` + +#### 4.5.3 使用示例 + +```typescript +import ParasiteExcess from 'oak-general-business/es/components/parasite/excess'; + +// 通常作为独立页面使用,路由配置: +// /parasite/excess + +// 页面组件: + +``` + +#### 4.5.4 生命周期流程 + +```mermaid +flowchart TD + A[组件 attached] --> B[setState loading=true] + B --> C[调用 cache.refresh 查询 Parasite] + C --> D{Parasite 是否存在?} + D -->|否| E[显示链接不合法] + D -->|是| F{是否已过期?} + F -->|是| G[显示链接已过期] + F -->|否| H[移除旧 Token] + H --> I[调用 token.wakeupParasite] + I --> J[redirectPage 跳转到目标页] + J --> K{用户昵称是否为shadow_user?} + K -->|是| L[不传递昵称参数] + K -->|否| M[传递用户昵称] + L --> N[redirectTo 跳转] + M --> N +``` + +#### 4.5.5 关键方法 + +**attached 生命周期** + +```typescript +async attached() { + const { oakId } = this.props; + this.setState({ loading: true }); + + try { + // 1. 查询 Parasite + const { data: [parasite] } = await this.features.cache.refresh('parasite', { + data: { + id: 1, expired: 1, expiresAt: 1, + entity: 1, entityId: 1, redirectTo: 1, + userId: 1, + user: { id: 1, nickname: 1 } + }, + filter: { id: oakId || 'illegal' } + }); + + // 2. 验证有效性 + if (!parasite) { + this.setState({ loading: false, illegal: true }); + return; + } + if (parasite.expired) { + this.setState({ loading: false, expired: true }); + return; + } + + // 3. 移除旧 Token 并生成新 Token + this.features.token.removeToken(); + await this.features.token.wakeupParasite(parasite.id!); + + // 4. 跳转到目标页面 + this.redirectPage( + parasite.redirectTo, + parasite?.user?.nickname === 'shadow_user' ? undefined : parasite?.user?.nickname + ); + } catch (err) { + this.setState({ loading: false }); + } +} +``` + +**redirectPage 方法** + +```typescript +redirectPage( + redirectTo?: EntityDict['parasite']['Schema']['redirectTo'], + nickname?: string | null +) { + if (!redirectTo) { + this.setMessage({ type: 'error', content: '未配置跳转页面' }); + return; + } + + const { pathname, props, state } = redirectTo; + const url = pathname.substring(0, 1) === '/' ? pathname : `/${pathname}`; + + this.redirectTo( + { + url, + ...(props || {}), + name: nickname, + oakId: this.props.oakId + }, + state + ); +} +``` + +### 4.6 Feature 方法 + +寄生模式在 Token Feature 中提供了前端调用方法。 + +#### 4.6.1 wakeupParasite + +```typescript +async wakeupParasite(id: string): Promise +``` + +**功能**:唤醒寄生链接,生成令牌并保存到本地存储。 + +**参数**: +- `id`: Parasite ID + +**使用示例**: + +```typescript +// 在组件中调用 +await this.features.token.wakeupParasite('parasite123'); + +// Token 已自动保存,用户已登录 +const user = this.features.token.getUserInfo(); +console.log(user.nickname); +``` + +### 4.7 完整使用示例 + +#### 4.7.1 创建寄生链接(后台管理页面) + +```typescript +import ParasiteList from 'oak-general-business/es/components/parasite/list'; +import ParasiteUpsert from 'oak-general-business/es/components/parasite/upsert'; + +function ActivityManagement() { + const activityId = 'activity123'; + + return ( +
+

活动邀请管理

+ + {/* 创建新的寄生链接 */} + + + {/* 显示已创建的寄生链接列表 */} + +
+ ); +} +``` + +#### 4.7.2 配置寄生入口页面(路由配置) + +```typescript +// app.tsx 或路由配置文件 +import ParasiteExcess from 'oak-general-business/es/components/parasite/excess'; + +const routes = [ + { + path: '/parasite/excess', + component: ParasiteExcess, + // 从 URL 获取 oakId 参数 + getProps: (query) => ({ oakId: query.oakId }) + } +]; +``` + +#### 4.7.3 分享寄生链接 + +```typescript +import ParasiteDetail from 'oak-general-business/es/components/parasite/detail'; + +function SharePage({ parasiteId }) { + return ( +
+

分享邀请

+

扫描二维码或复制链接分享给好友:

+ + +
+ ); +} +``` + +--- + +**参考代码路径**: +- ParasiteList: `src/components/parasite/list/index.ts` +- ParasiteDetail: `src/components/parasite/detail/index.ts` +- ParasiteUpsert: `src/components/parasite/upsert/index.ts` +- ParasiteExcess: `src/components/parasite/excess/index.ts` +- Token Feature: `src/features/token.ts` (第 466-475 行) + +--- + +## 5. 接入指南 + +### 5.1 前置条件 + +使用寄生模式功能前,需要确保以下功能已正确配置: + +1. **用户系统**:已接入用户注册和登录功能 +2. **令牌管理**:Token 功能正常工作 +3. **影子用户**:能够创建 `userState='shadow'` 的用户 +4. **用户关系**:如果需要限定用户范围,需要配置 UserRelation + +### 5.2 配置步骤 + +#### 步骤 1:添加寄生入口页面路由 + +在应用的路由配置中添加寄生链接入口页面: + +```typescript +// routes.ts +import ParasiteExcess from 'oak-general-business/es/components/parasite/excess'; + +export const routes = [ + // ... 其他路由 + { + path: '/parasite/excess', + component: ParasiteExcess, + getProps: (query) => ({ + oakId: query.oakId // 从 URL 获取 Parasite ID + }) + } +]; +``` + +**注意**: +- 路径必须为 `/parasite/excess`,因为组件内部生成的链接使用此路径 +- 如需自定义路径,需要同时修改 `ParasiteDetail` 组件中的 URL 生成逻辑 + +#### 步骤 2:创建影子用户 + +寄生模式依赖影子用户,需要在业务逻辑中预先创建: + +```typescript +// 方式一:通过 Aspect 创建 +const { result: userId } = await cache.exec('createUser', { + data: { + nickname: 'shadow_user', // 或其他昵称 + userState: 'shadow' // 必须为 shadow + } +}); + +// 方式二:直接操作实体 +await context.operate('user', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + nickname: 'shadow_user', + userState: 'shadow' + } +}); +``` + +#### 步骤 3:(可选)配置用户关系 + +如果希望限定寄生链接只能分配给特定关系的影子用户(如活动参与者),需要创建用户关系: + +```typescript +await context.operate('userRelation', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + userId: shadowUserId, // 影子用户 ID + entity: 'activity', // 关联实体类型 + entityId: 'activity123', // 关联实体 ID + relation: { + name: 'activity_participant' // 关系名称 + } + } +}); +``` + +#### 步骤 4:创建寄生记录 + +```typescript +// 后端创建 +await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + userId: shadowUserId, + entity: 'activity', + entityId: 'activity123', + expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7天后过期 + expired: false, + multiple: false, // 单次使用 + tokenLifeLength: 30 * 24 * 60 * 60 * 1000, // Token 30天有效期 + redirectTo: { + pathname: '/activity/detail', + props: { id: 'activity123' }, + state: { fromParasite: true } + } + } +}); + +// 或使用前端组件创建(推荐) + +``` + +#### 步骤 5:分享寄生链接 + +```typescript +// 获取 Parasite ID 后,生成链接 +const parasiteUrl = `${window.location.origin}/parasite/excess?oakId=${parasiteId}`; + +// 使用 ParasiteDetail 组件展示链接和二维码 + +``` + +### 5.3 完整业务流程示例 + +以活动邀请为例,展示完整的接入流程: + +#### 5.3.1 后端:创建活动和影子用户 + +```typescript +// 1. 创建活动 +const activityId = await generateNewIdAsync(); +await context.operate('activity', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: activityId, + title: '春游活动', + // ... 其他活动信息 + } +}); + +// 2. 为每个参与名额创建影子用户 +const shadowUsers = []; +for (let i = 0; i < 10; i++) { // 假设有10个名额 + const shadowUserId = await generateNewIdAsync(); + + // 创建影子用户 + await context.operate('user', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: shadowUserId, + nickname: `活动参与者${i + 1}`, + userState: 'shadow' + } + }); + + // 建立用户关系 + await context.operate('userRelation', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + userId: shadowUserId, + entity: 'activity', + entityId: activityId, + relation: { name: 'participant' } + } + }); + + // 创建寄生记录 + const parasiteId = await generateNewIdAsync(); + await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: parasiteId, + userId: shadowUserId, + entity: 'activity', + entityId: activityId, + expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, + expired: false, + multiple: false, + tokenLifeLength: 30 * 24 * 60 * 60 * 1000, + redirectTo: { + pathname: '/activity/join', + props: { id: activityId } + } + } + }); + + shadowUsers.push({ shadowUserId, parasiteId }); +} + +return shadowUsers; +``` + +#### 5.3.2 前端:管理页面 + +```typescript +import ParasiteList from 'oak-general-business/es/components/parasite/list'; +import ParasiteUpsert from 'oak-general-business/es/components/parasite/upsert'; + +function ActivityManagePage({ activityId }) { + return ( +
+

活动邀请管理

+ + {/* 创建新的邀请链接 */} +
+

创建邀请链接

+ +
+ + {/* 已创建的邀请链接列表 */} +
+

已创建的邀请链接

+ +
+
+ ); +} +``` + +#### 5.3.3 前端:活动参与页面 + +```typescript +// /activity/join +import { useEffect } from 'react'; +import { useToken } from 'oak-frontend-base'; + +function ActivityJoinPage({ id }) { + const token = useToken(); + const user = token.getUserInfo(); + + useEffect(() => { + // 用户已通过寄生链接自动登录 + console.log('当前用户:', user.nickname); + console.log('用户状态:', user.userState); // 'shadow' + }, []); + + const handleJoin = async () => { + // 用户填写信息后激活账号 + await cache.exec('activateUser', { + userId: user.id, + realName: form.realName, + mobile: form.mobile, + // ... 其他信息 + }); + + // 激活后,所有相关的寄生记录会自动作废 + // 用户状态变为 'normal' + }; + + return ( +
+

欢迎参加活动

+

您正在以临时身份访问,请填写信息完成注册:

+ {/* 表单 */} +
+ ); +} +``` + +### 5.4 常见配置 + +#### 5.4.1 设置链接有效期 + +```typescript +// 7天后过期 +expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000 + +// 1小时后过期 +expiresAt: Date.now() + 60 * 60 * 1000 + +// 指定具体时间 +expiresAt: new Date('2025-12-31').getTime() +``` + +#### 5.4.2 设置 Token 有效期 + +```typescript +// Token 30天有效期 +tokenLifeLength: 30 * 24 * 60 * 60 * 1000 + +// Token 永久有效(不推荐) +tokenLifeLength: 999999999999 + +// 使用系统默认配置 +tokenLifeLength: undefined +``` + +#### 5.4.3 允许重复使用 + +```typescript +// 单次使用(默认) +multiple: false + +// 允许重复使用 +multiple: true +``` + +#### 5.4.4 配置跳转页面 + +```typescript +// 简单跳转 +redirectTo: { + pathname: '/home' +} + +// 带查询参数 +redirectTo: { + pathname: '/activity/detail', + props: { id: 'activity123', from: 'share' } +} + +// 带状态(不显示在 URL) +redirectTo: { + pathname: '/activity/detail', + props: { id: 'activity123' }, + state: { fromParasite: true, inviter: 'user456' } +} +``` + +### 5.5 注意事项 + +1. **影子用户限制** + - 只有 `userState='shadow'` 的用户才能关联 Parasite + - 用户激活后,所有相关 Parasite 自动作废 + +2. **过期时间管理** + - 系统不会自动清理过期的 Parasite,只是标记为过期 + - 建议定期清理过期记录 + +3. **Token 生命周期** + - `tokenLifeLength` 影响生成的 Token 的有效期 + - 如果不设置,使用系统默认配置 + +4. **单次使用模式** + - `multiple=false` 时,点击链接后立即失效 + - 适用于一次性邀请场景 + +5. **路由配置** + - 必须配置 `/parasite/excess` 路由 + - 或修改 `ParasiteDetail` 组件中的 URL 生成逻辑 + +6. **权限控制** + - 寄生模式内部使用 `rootMode`,绕过权限检查 + - 业务方需要在创建 Parasite 时做好权限控制 + +--- + +**参考代码路径**: +- 实体定义:`src/entities/Parasite.ts` +- 组件示例:`src/components/parasite/` + +--- + +## 6. API 接口说明 + +### 6.1 Aspect 方法 + +寄生模式通过 Aspect 提供了后端业务逻辑调用接口。 + +#### 6.1.1 wakeupParasite + +**方法签名**: + +```typescript +async function wakeupParasite( + params: { + id: string; // Parasite ID + env: WebEnv | WechatMpEnv | NativeEnv; // 登录环境 + }, + context: BRC +): Promise // 返回 Token 值 +``` + +**功能**:唤醒寄生链接,为影子用户生成令牌。 + +**参数说明**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `id` | string | 是 | Parasite 记录的 ID | +| `env` | WebEnv \| WechatMpEnv \| NativeEnv | 是 | 用户的登录环境,用于标识 Token 的使用平台 | + +**返回值**: +- 成功:返回生成的 Token 字符串 +- 失败:抛出异常 + +**异常**: + +| 异常类型 | 触发条件 | 说明 | +|---------|---------|------| +| `OakRowInconsistencyException` | `parasite.expired === true` | 寄生链接已过期或作废 | +| `OakUserException` | `parasite.user.userState !== 'shadow'` | 用户已激活,不允许使用寄生链接 | + +**使用示例**: + +```typescript +// 后端调用 +const tokenValue = await wakeupParasite( + { + id: 'parasite123', + env: { type: 'web', platform: 'pc' } + }, + context +); + +// tokenValue: 生成的 Token 字符串 +``` + +**前端调用**(通过 Feature): + +```typescript +// 在组件或 Feature 中 +await this.features.token.wakeupParasite('parasite123'); + +// Token 已自动保存到本地存储 +const user = this.features.token.getUserInfo(); +``` + +### 6.2 Endpoint 接口 + +寄生模式没有对外暴露的 RESTful API Endpoint,所有交互通过前端组件和 Aspect 完成。 + +### 6.3 实体操作 + +可以通过标准的实体操作来管理 Parasite 记录。 + +#### 6.3.1 创建 Parasite + +```typescript +await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'create', + data: { + id: await generateNewIdAsync(), + userId: 'shadowUser123', + entity: 'activity', + entityId: 'activity456', + expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, + expired: false, + multiple: false, + tokenLifeLength: 30 * 24 * 60 * 60 * 1000, + redirectTo: { + pathname: '/activity/detail', + props: { id: 'activity456' } + }, + showTip: true + } +}); +``` + +#### 6.3.2 查询 Parasite + +```typescript +const parasites = await context.select('parasite', { + data: { + id: 1, + userId: 1, + entity: 1, + entityId: 1, + expired: 1, + expiresAt: 1, + redirectTo: 1, + user: { + id: 1, + nickname: 1, + userState: 1 + } + }, + filter: { + entity: 'activity', + entityId: 'activity456', + expired: false + } +}); +``` + +#### 6.3.3 更新 Parasite(标记过期) + +```typescript +await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'update', + data: { + expired: true + }, + filter: { + id: 'parasite123' + } +}); +``` + +#### 6.3.4 作废 Parasite + +```typescript +await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'cancel', + data: { + expired: true + }, + filter: { + id: 'parasite123' + } +}); +``` + +**注意**:`cancel` 动作会触发相关 Token 的禁用。 + +#### 6.3.5 生成二维码 + +```typescript +await context.operate('parasite', { + id: await generateNewIdAsync(), + action: 'qrcode', + data: {}, + filter: { + id: 'parasite123' + } +}); +``` + +**注意**:`qrcode` 动作本身不执行任何逻辑,主要用于前端组件显示二维码。 + +### 6.4 前端 Feature 方法 + +#### 6.4.1 wakeupParasite + +```typescript +class Token extends Feature { + async wakeupParasite(id: string): Promise +} +``` + +**功能**:前端调用,唤醒寄生链接并保存 Token。 + +**参数**: +- `id`: Parasite ID + +**使用示例**: + +```typescript +// 在组件中 +await this.features.token.wakeupParasite('parasite123'); + +// Token 已保存,可以获取用户信息 +const userInfo = this.features.token.getUserInfo(); +console.log(userInfo.nickname); +``` + +**内部流程**: +1. 获取当前环境(Web/小程序/Native) +2. 调用 `wakeupParasite` Aspect +3. 保存返回的 Token 到本地存储 +4. 发布 Token 更新事件 + +### 6.5 缓存查询 + +可以通过 `cache.refresh` 查询 Parasite 数据: + +```typescript +const { data } = await this.features.cache.refresh('parasite', { + data: { + id: 1, + expired: 1, + expiresAt: 1, + entity: 1, + entityId: 1, + redirectTo: 1, + user: { + id: 1, + nickname: 1 + } + }, + filter: { + id: 'parasite123' + } +}); + +const parasite = data[0]; +``` + +### 6.6 常用查询示例 + +#### 6.6.1 查询某个用户的所有 Parasite + +```typescript +const parasites = await context.select('parasite', { + data: { + id: 1, + entity: 1, + entityId: 1, + expired: 1, + expiresAt: 1 + }, + filter: { + userId: 'shadowUser123', + expired: false + } +}); +``` + +#### 6.6.2 查询某个实体的所有 Parasite + +```typescript +const parasites = await context.select('parasite', { + data: { + id: 1, + user: { + id: 1, + nickname: 1 + }, + expired: 1, + expiresAt: 1 + }, + filter: { + entity: 'activity', + entityId: 'activity456' + } +}); +``` + +#### 6.6.3 查询已过期的 Parasite + +```typescript +const expiredParasites = await context.select('parasite', { + data: { + id: 1, + entity: 1, + entityId: 1, + expiresAt: 1 + }, + filter: { + expired: true + } +}); +``` + +#### 6.6.4 查询即将过期的 Parasite + +```typescript +const soonExpireParasites = await context.select('parasite', { + data: { + id: 1, + entity: 1, + entityId: 1, + expiresAt: 1 + }, + filter: { + expired: false, + expiresAt: { + $lt: Date.now() + 24 * 60 * 60 * 1000 // 24小时内过期 + } + } +}); +``` + +### 6.7 权限说明 + +寄生模式相关操作在内部使用了 `context.openRootMode()`,这意味着: + +1. **绕过权限检查**:不受常规权限系统限制 +2. **业务方责任**:业务方需要在创建 Parasite 时做好权限控制 +3. **安全建议**: + - 创建 Parasite 前验证用户身份 + - 限制 Parasite 的创建数量 + - 设置合理的过期时间 + - 记录创建日志 + +--- + +**参考代码路径**: +- Aspect 定义:`src/aspects/token.ts` (第 3091-3176 行) +- Aspect 导出:`src/aspects/index.ts` +- Feature 实现:`src/features/token.ts` (第 466-475 行) + +--- + +**最后更新**:2025年10月26日 + +**版本**:v1.0 + +**维护者**:oak-general-business 开发团队