引导计划(Campaign)把已发布的 GuidanceContent 组织成一条有序、有受众、有排期的多步引导,并为每个用户跟踪独立进度。它和单纯的上下文指南不是一回事:上下文指南是”用户走到某个页面时展示一段帮助”,引导计划是”运营主动投放一组步骤,引导某类用户完成一条路径”。
1. 两个聚合根
GuidanceCampaign 是租户级聚合根,组合一段有序的已发布 GuidanceContent 步骤 ID 列表,加上受众定向规则和排期窗口。GuidanceCampaignAssignment 是用户级的分配与进度聚合根,按 (TenantId, CampaignId, UserId) 唯一(索引 UX_GuidanceCampaignAssignment_Tenant_Campaign_User)。
2. 计划状态机
GuidanceCampaignStatus 有四态:
| 状态 | 含义 |
|---|---|
Draft | 编辑中,对终端用户不可见 |
Active | 已激活,符合受众与排期的用户可见 |
Paused | 暂停投放,已分配的进度保留 |
Archived | 归档,不再投放 |
状态迁移由聚合严格守卫,违反时返回对应的 Guidance.Campaign.* 错误:
Create产生Draft;Configure(更新步骤、受众、排期)只允许Draft或Paused,否则Guidance.Campaign.MustBeEditable;Activate只允许从Draft或Paused,且会拒绝已过期排期(Guidance.Campaign.ScheduleExpired);Pause只允许从Active(Guidance.Campaign.TransitionInvalid);Archive允许从任何非归档态。
每次生命周期变更都必须带 LastChangeReason(最长 512),作为有界审计字段。
3. 受众与排期
受众定向不依赖单一角色字符串,而是组合以下字段:
| 字段 | 限制 | 语义 |
|---|---|---|
TargetRoleNames | 最多 50 个 | 命中任一角色即符合 |
TargetUserIds | 最多 200 个 | 显式点名用户 |
TargetsAllUsers | 计算字段 | 两者皆空时为 true,即面向全租户 |
IsEligible(userId, roles, now) 把状态、排期窗口和角色/用户成员关系合并判定。排期由 StartsAt(空表示立即生效)和 EndsAt(空表示不自动过期)控制,数据库 CHECK 约束 CK_GuidanceCampaign_Schedule 保证 StartsAt < EndsAt。激活时若窗口已经结束,会被 Guidance.Campaign.ScheduleExpired 拒绝。
激活一个计划时,系统会为显式点名的目标用户预创建分配(EnsureExplicitAssignmentsAsync);面向全租户或按角色的计划则在用户首次交互时隐式创建分配(CreateAssignmentAsync)。
4. 有序多步与进度状态机
步骤是有序的 ContentIds 列表,上限 StepLimit = 20,每个标识符最长 64 字符。用户进度由 GuidanceProgressStatus 描述:
| 状态 | 含义 |
|---|---|
NotStarted | 已分配但未开始 |
InProgress | 完成至少一步但未全部完成 |
Completed | 全部步骤完成 |
Dismissed | 用户主动豁免 |
进度推进是服务端权威的,不是客户端自行声称:
CompleteStep幂等。首次完成任意一步会把NotStarted推进到InProgress;全部步骤完成后自动推进到Completed。重复完成同一步不报错;重复完成整个计划返回Guidance.Campaign.AlreadyCompleted。Dismiss幂等,但受计划级AllowDismiss控制。AllowDismiss为 false 时返回Guidance.Campaign.DismissDenied;已经豁免再豁免返回Guidance.Campaign.AlreadyDismissed。- 找不到步骤返回
Guidance.Campaign.StepNotFound。
这条”服务端权威 + 幂等 + AllowDismiss”的链路保证进度可信:客户端不能伪造完成,也不能绕过豁免策略。
5. HTTP 路由
计划路由集中在 /api/v1/guidance/campaigns,由 Source Generator 生成:
| 方法与路由 | 用途 | 权限 |
|---|---|---|
POST /api/v1/guidance/campaigns | 创建计划 | guidance.campaign.manage |
GET /api/v1/guidance/campaigns | 管理列表 | guidance.campaign.read |
GET /api/v1/guidance/campaigns/{campaignId} | 计划详情 | guidance.campaign.read |
PUT /api/v1/guidance/campaigns/{campaignId} | 更新步骤/受众/排期 | guidance.campaign.manage |
POST /api/v1/guidance/campaigns/{campaignId}/activate | 激活 | guidance.campaign.manage |
POST /api/v1/guidance/campaigns/{campaignId}/pause | 暂停 | guidance.campaign.manage |
POST /api/v1/guidance/campaigns/{campaignId}/archive | 归档 | guidance.campaign.manage |
GET /api/v1/guidance/campaigns/{campaignId}/analytics | 完成率与分布分析 | guidance.campaign.read |
GET /api/v1/guidance/campaigns/me | 当前用户可见计划 | guidance.campaign.use |
POST /api/v1/guidance/campaigns/me/{campaignId}/steps/{contentId}/complete | 完成一步 | guidance.campaign.use |
POST /api/v1/guidance/campaigns/me/{campaignId}/dismiss | 豁免计划 | guidance.campaign.use |
管理类操作(创建、更新、状态变更、读取、分析)走 guidance.campaign.read / guidance.campaign.manage;终端用户操作(查看自己的计划、完成步骤、豁免)走 guidance.campaign.use。三类权限各司其职,运营不会误用终端权限,终端用户也不会获得管理能力。
6. 分析口径
GET /api/v1/guidance/campaigns/{campaignId}/analytics 返回 GuidanceCampaignAnalytics,给出真实的(非估算的)进度分布计数:NotStarted、InProgress、Completed、Dismissed,以及 CompletionRate。这些数字来自分配聚合的实际状态,不是采样或估算。控制台可以直接据此渲染漏斗。
7. 持久化
SQL Server 迁移 202607300004-guidance-campaigns.sql 创建两张表:
dbo.GuidanceCampaign,带CK_GuidanceCampaign_Status('Draft','Active','Paused','Archived')和CK_GuidanceCampaign_Schedule,索引按Tenant/Status和Tenant/Language。dbo.GuidanceCampaignAssignment,带CK_GuidanceCampaignAssignment_Status('NotStarted','InProgress','Completed','Dismissed')和唯一索引UX_GuidanceCampaignAssignment_Tenant_Campaign_User,查询索引按Tenant/Campaign/Status。
GuidanceCampaignStore 在 Infrastructure 层实现 IGuidanceCampaignStore,支撑上述命令与查询。Guidance Campaign 与 GuidanceContent、Session 一样有 SqlSugar/EF Core parity 基础。
8. 与上下文指南的区别
容易混淆的两个概念:
- 上下文指南(
GuidanceContent+ Contextual Query):被动展示,用户走到某个路由/组件时按四级匹配取一段已发布帮助,受众只靠单个RequiredRole字符串。 - 引导计划(
GuidanceCampaign):主动投放,运营把多个已发布内容串成有序步骤,按角色/用户/全租户定向,带排期和服务端权威进度。
两者共享 GuidanceContent 作为内容来源,但计划只是引用已发布内容的 ID,不复制正文。因此内容的发布生命周期(Draft/Published)和计划的生命周期(Draft/Active/Paused/Archived)是独立的:一个计划引用的内容被撤销发布,会影响该计划步骤的可读性。
9. 错误码一览
计划相关的错误码集中在 Guidance.Campaign.* 命名空间,代表性的包括:
| 错误码 | 触发场景 |
|---|---|
Guidance.Campaign.Invalid | 输入校验失败 |
Guidance.Campaign.TenantRequired | 缺少可信租户上下文 |
Guidance.Campaign.NotFound | 计划不存在 |
Guidance.Campaign.ContentInvalid | 步骤内容引用非法 |
Guidance.Campaign.MustBeEditable | 在非可编辑态执行 Configure |
Guidance.Campaign.TransitionInvalid | 非法状态迁移 |
Guidance.Campaign.ScheduleExpired | 激活已过期排期 |
Guidance.Campaign.NotEligible | 用户不符合受众 |
Guidance.Campaign.StepNotFound | 步骤不存在 |
Guidance.Campaign.AlreadyDismissed | 重复豁免 |
Guidance.Campaign.DismissDenied | 计划不允许豁免 |
Guidance.Campaign.AlreadyCompleted | 重复完成 |
Guidance.Campaign.VersionConflict | 乐观并发冲突 |
10. 源码核查
# 计划聚合、状态机、分配与进度。rg -n "GuidanceCampaign|GuidanceCampaignStatus|GuidanceProgressStatus|GuidanceCampaignAssignment" \ src/Platform/Guidance -g '*.cs'
# 计划权限码与路由。rg -n "guidance.campaign|CampaignResource|HttpRoute" \ src/Platform/Guidance -g '*.cs'
# 物理表与约束。rg -n "GuidanceCampaign|CK_GuidanceCampaign" \ src/Hosts/BitzOrcas.Api/SchemaMigrations/SqlServer/202607300004-guidance-campaigns.sql