diff --git a/client/src/api/portal.ts b/client/src/api/portal.ts
index e37259ca..41d12072 100644
--- a/client/src/api/portal.ts
+++ b/client/src/api/portal.ts
@@ -29,6 +29,21 @@ export type PortalHomePayload = {
weeklySummary: { weekStart: string; submittedCount: number; missingCount: number; needsHelpCount: number; href: string } | null;
todoSummary: { openCount: number; highPriorityCount: number; href: string } | null;
peopleLifecycle: { onboardingCount: number; offboardingCount: number; assetReturnPendingCount: number; href: string } | null;
+ adoptionSummary:
+ | {
+ activeEmployeeCount: number;
+ login7dCount: number;
+ weeklySubmittedCount: number;
+ weeklyMissingCount: number;
+ presenceUpdatedCount: number;
+ presenceMissingCount: number;
+ announcementCount: number;
+ login7dRate: number;
+ weeklySubmissionRate: number;
+ presenceCoverageRate: number;
+ hints: string[];
+ }
+ | null;
};
export async function fetchPortalHome() {
diff --git a/client/src/pages/PortalHomePage.tsx b/client/src/pages/PortalHomePage.tsx
index 7c8f2b12..07b4fa61 100644
--- a/client/src/pages/PortalHomePage.tsx
+++ b/client/src/pages/PortalHomePage.tsx
@@ -131,6 +131,41 @@ export function PortalHomePage() {
+ {payload.adoptionSummary ? (
+
工作台摘要
diff --git a/deploy/trialrun/checklist.md b/deploy/trialrun/checklist.md
index b8122cb0..a37afe16 100644
--- a/deploy/trialrun/checklist.md
+++ b/deploy/trialrun/checklist.md
@@ -65,4 +65,4 @@
- [ ] 至少 1 条公告已发布并在 `Portal` 可见
- [ ] 至少 1 次入职/离职/资产交接已演练
- [ ] 全员已知晓每周周报和今日状态使用要求
-- [ ] 已建立连续 4 周观测记录方式
+- [ ] 已建立连续 4 周观测记录方式(建议直接使用 `deploy/trialrun/observation-template.md`)
diff --git a/deploy/trialrun/observation-template.md b/deploy/trialrun/observation-template.md
new file mode 100644
index 00000000..bd3462f5
--- /dev/null
+++ b/deploy/trialrun/observation-template.md
@@ -0,0 +1,37 @@
+# 小团队试点 4 周观测模板
+
+> 建议由管理员或老板每周五填写一次,用于 `R17-6` 最终验收留痕。
+
+## 基本信息
+- 工作区模式:`small_team_internal`
+- 观测周期:第 __ 周(起止日期:____ 至 ____)
+- 填写人:____
+
+## 量化指标
+| 指标 | 本周值 | 目标 | 结论 |
+| --- | --- | --- | --- |
+| 近 7 天登录人数 / 在职人数 | __ / __ | 全员每周至少登录 1 次 | __ |
+| 本周周报提交人数 / 在职人数 | __ / __ | 全员提交 | __ |
+| 今日状态更新人数 / 在职人数 | __ / __ | 全员工作日可见 | __ |
+| 在岗表是否已停止外部维护 | 是 / 否 | 是 | __ |
+| 离职交接表是否已停止外部维护 | 是 / 否 | 是 | __ |
+
+## 本周关键动作
+- 公告 / Portal / 周报 / 待办 的使用情况:
+- 入职 / 离职 / 资产交接是否发生:
+- 仍依赖聊天或表格补流程的场景:
+
+## 本周阻塞
+- 阻塞 1:
+- 阻塞 2:
+- 阻塞 3:
+
+## 下周调整
+- 是否需要调整提醒频率:
+- 是否需要继续隐藏或降级某模块:
+- 是否需要补充培训或说明:
+
+## 结论
+- 本周是否达到试点预期:是 / 否
+- 若否,主要原因:
+- 最终验收建议:继续观测 / 可进入验收
diff --git a/deploy/trialrun/runbook.md b/deploy/trialrun/runbook.md
index fb35cfb3..baae9711 100644
--- a/deploy/trialrun/runbook.md
+++ b/deploy/trialrun/runbook.md
@@ -224,4 +224,5 @@ docker logs -f hyb-oa-trialrun-server
- 团队状态更新人数
- 公告阅读反馈
- 入职/离职/资产交接演练情况
+- 建议统一使用 `deploy/trialrun/observation-template.md` 填写每周结论与阻塞。
- 若连续两周无人使用某入口,考虑继续降级模块可见性。
diff --git a/docs/admin-handbook.md b/docs/admin-handbook.md
index f523bea4..76e29a06 100644
--- a/docs/admin-handbook.md
+++ b/docs/admin-handbook.md
@@ -74,3 +74,9 @@
- 至少 1 次真实离职或模拟离职跑通
- 全员连续 4 周提交周报
- 管理员不再额外维护“今日在岗表”或“离职交接表”
+
+## 11. 4 周观测操作法
+1. 每周一进入 `Portal` 查看“试点观测摘要”,记录近 7 天登录覆盖率、周报提交率、团队状态覆盖率。
+2. 每周五在 `deploy/trialrun/observation-template.md` 填写本周结论、关键阻塞、是否需要调整提醒或导航。
+3. 若某周有员工未登录、未提周报、未更新状态,优先通过公告或站内通知补提醒,不引入额外表格。
+4. 连续 4 周都达到预期后,再将 `R17-6` 关单并形成最终验收结论。
diff --git a/docs/feature-matrix.md b/docs/feature-matrix.md
index de373265..9435993a 100644
--- a/docs/feature-matrix.md
+++ b/docs/feature-matrix.md
@@ -345,4 +345,5 @@
- **R15 团队状态**:新增 `EmployeePresence` 与团队在岗视图,支持员工手工更新今日状态,请假自动覆盖为 `leave`。
- **R16 周报与待办**:新增 `WeeklyReport` 与 `TodoItem`,支持周报懒创建、提交、团队汇总、待办创建/完成/取消,并接入 `Portal` 与搜索。
- **R17 真可用收口**:新增小团队管理员手册、员工快速上手文档、trialrun 运行手册与检查项补充,支持以 `small_team_internal` 为默认上线模式。
+- **R17-4 试点观测支撑**:`Portal` 新增管理员试点观测摘要(近 7 天登录、周报提交、团队状态覆盖率),并补充 4 周观测模板。
- **默认上线建议**:开启 `portal_home_v1`、`announcements_v1`、`presence_board_v1`、`weekly_report_v1`、`employee_lifecycle_v1`、`todo_board_v1`、`global_search_v1`;保留 `payment_batch_v1`、`cutover_console_v1` 为管理员备用能力。
diff --git a/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv b/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv
index a9e40861..d04769a8 100644
--- a/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv
+++ b/issues/2026-03-21_12-40-00-r17-small-team-go-live.csv
@@ -1,8 +1,8 @@
-ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
-R17-0,R17执行合同建档与上线边界冻结,创建并校验小团队上线收口合同 CSV 并冻结本阶段只做默认配置 放量 文档 手册 试点和验收,CSV 校验通过且明确目标是小团队真正可用而不是继续扩展新模块,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-21_12-40-00-r17-small-team-go-live.csv,manual,DONE,DONE,DONE,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|issues/2026-03-21_12-30-00-r16-weekly-report-todo.csv,[],none
-R17-1,默认配置与功能开关收口,为 trialrun 和 seed 打开 small_team_internal profile 与核心小团队功能开关 并默认降级重模块,trialrun 默认开启 Portal 公告 周报 待办 团队状态 人员生命周期 并默认关闭 payment batch cutover 与重模块主导航,cd server && npm test && git diff --check,manual,DONE,DONE,DONE,server/src/scripts/seed-config.js|server/.env.example|deploy/trialrun/.env.example|server/src/services/featureFlags/index.js,"[""R17-0""]",none
-R17-2,存量员工与资产数据整理支撑,提供小团队上线所需的人员档案 资产归属和部门关系整理支撑接口或导入校验,管理员可在上线前核对员工档案 部门关系和资产归属缺口,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/employees.js|server/src/routes/assets.js|client/src/pages/PeoplePage.tsx|client/src/pages/AssetRegistryPage.tsx,"[""R17-1""]",none
-R17-3,管理员手册与员工使用手册,新增 admin-handbook 和 employee-quickstart 并补齐试运行 runbook/checklist 的小团队模式步骤,管理员和员工均有可执行手册 支撑首次配置 入职 离职 周报与团队状态的真实演练,git diff --check,manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|deploy/trialrun/runbook.md|deploy/trialrun/checklist.md,"[""R17-1""]",none
-R17-4,全员试点与观测留痕,补齐试点观测字段 页面或文档留痕 支撑 4 周使用观测和真实验收记录,系统可记录或沉淀全员使用情况 周报提交率 关键阻塞和试点观察结论,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/portal.js|client/src/pages/PortalHomePage.tsx|docs/admin-handbook.md|docs/feature-matrix.md,"[""R17-2"",""R17-3""]",none
-R17-5,默认收口无用主导航模块,按小团队产品模式彻底收口全员默认导航 仅为管理员保留备用企业模块入口,全员默认主导航仅剩 Portal 团队 请假 报销 资产 周报 待办 公告 管理员附加入口符合约定,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/router/index.tsx|client/src/pages/PortalHomePage.tsx,"[""R17-1""]",none
-R17-6,R17最终验收,完成默认配置 文档手册 试点观测和主导航收口后的最终验收,全员账户 档案 部门关系已补齐 入职离职资产交接至少各跑通一次 全员连续使用周报与团队状态且主回归 E2E 全绿,cd server && npm test && ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,TODO,TODO,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|server/scripts/regression-all.sh|client/tests/e2e|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R17-4"",""R17-5""]",代码/文档/回归基线已完成;仍待真实 4 周试点观测与全员连续使用证据后再关单
+ID,Title,Description,Acceptance,Test_Method,Tools,Dev_Status,Review1_Status,Regression_Status,Files,Dependencies,Notes
+R17-0,R17执行合同建档与上线边界冻结,创建并校验小团队上线收口合同 CSV 并冻结本阶段只做默认配置 放量 文档 手册 试点和验收,CSV 校验通过且明确目标是小团队真正可用而不是继续扩展新模块,python3 /home/shunli/.codex/skills/plan/scripts/validate_issues_csv.py issues/2026-03-21_12-40-00-r17-small-team-go-live.csv,manual,DONE,DONE,DONE,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|issues/2026-03-21_12-30-00-r16-weekly-report-todo.csv,[],none
+R17-1,默认配置与功能开关收口,为 trialrun 和 seed 打开 small_team_internal profile 与核心小团队功能开关 并默认降级重模块,trialrun 默认开启 Portal 公告 周报 待办 团队状态 人员生命周期 并默认关闭 payment batch cutover 与重模块主导航,cd server && npm test && git diff --check,manual,DONE,DONE,DONE,server/src/scripts/seed-config.js|server/.env.example|deploy/trialrun/.env.example|server/src/services/featureFlags/index.js,"[""R17-0""]",none
+R17-2,存量员工与资产数据整理支撑,提供小团队上线所需的人员档案 资产归属和部门关系整理支撑接口或导入校验,管理员可在上线前核对员工档案 部门关系和资产归属缺口,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/employees.js|server/src/routes/assets.js|client/src/pages/PeoplePage.tsx|client/src/pages/AssetRegistryPage.tsx,"[""R17-1""]",none
+R17-3,管理员手册与员工使用手册,新增 admin-handbook 和 employee-quickstart 并补齐试运行 runbook/checklist 的小团队模式步骤,管理员和员工均有可执行手册 支撑首次配置 入职 离职 周报与团队状态的真实演练,git diff --check,manual,DONE,DONE,DONE,docs/admin-handbook.md|docs/employee-quickstart.md|deploy/trialrun/runbook.md|deploy/trialrun/checklist.md,"[""R17-1""]",none
+R17-4,全员试点与观测留痕,补齐试点观测字段 页面或文档留痕 支撑 4 周使用观测和真实验收记录,系统可记录或沉淀全员使用情况 周报提交率 关键阻塞和试点观察结论,cd server && npm test && cd ../client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,server/src/routes/portal.js|client/src/pages/PortalHomePage.tsx|docs/admin-handbook.md|docs/feature-matrix.md|deploy/trialrun/observation-template.md,"[""R17-2"",""R17-3""]",Portal 已提供试点观测摘要并补齐 4 周观测模板
+R17-5,默认收口无用主导航模块,按小团队产品模式彻底收口全员默认导航 仅为管理员保留备用企业模块入口,全员默认主导航仅剩 Portal 团队 请假 报销 资产 周报 待办 公告 管理员附加入口符合约定,cd client && npm run typecheck && npm run build,manual,DONE,DONE,DONE,client/src/components/navigation/NavLinks.tsx|client/src/router/index.tsx|client/src/pages/PortalHomePage.tsx,"[""R17-1""]",none
+R17-6,R17最终验收,完成默认配置 文档手册 试点观测和主导航收口后的最终验收,全员账户 档案 部门关系已补齐 入职离职资产交接至少各跑通一次 全员连续使用周报与团队状态且主回归 E2E 全绿,cd server && npm test && ./scripts/regression-all.sh && cd ../client && npm run typecheck && npm run build && npm run test:e2e,manual,DOING,TODO,TODO,issues/2026-03-21_12-40-00-r17-small-team-go-live.csv|server/scripts/regression-all.sh|client/tests/e2e|docs/admin-handbook.md|docs/employee-quickstart.md,"[""R17-4"",""R17-5""]",代码/文档/回归基线已完成;仍待真实 4 周试点观测与全员连续使用证据后再关单
diff --git a/server/src/routes/portal.js b/server/src/routes/portal.js
index cfed1134..eb2d7e6b 100644
--- a/server/src/routes/portal.js
+++ b/server/src/routes/portal.js
@@ -146,6 +146,26 @@ function buildQuickLinks({ request, featureEnabled }) {
return items.slice(0, 8);
}
+function buildAdoptionHints({ activeEmployeeCount, login7dCount, weeklySummary, teamPresenceSummary, offboardingCount, assetReturnPendingCount }) {
+ const hints = [];
+ if (login7dCount < activeEmployeeCount) {
+ hints.push(`近 7 天仍有 ${Math.max(0, activeEmployeeCount - login7dCount)} 人未登录系统`);
+ }
+ if ((weeklySummary?.missingCount ?? 0) > 0) {
+ hints.push(`本周仍有 ${weeklySummary.missingCount} 人未提交周报`);
+ }
+ if ((teamPresenceSummary?.unspecified ?? 0) > 0) {
+ hints.push(`今日仍有 ${teamPresenceSummary.unspecified} 人未更新团队状态`);
+ }
+ if ((offboardingCount ?? 0) > 0 || (assetReturnPendingCount ?? 0) > 0) {
+ hints.push(`仍有 ${offboardingCount ?? 0} 条离职流程和 ${assetReturnPendingCount ?? 0} 项资产交接待收口`);
+ }
+ if (!hints.length) {
+ hints.push("近 7 天登录、周报、团队状态与离职交接均满足试点观测口径");
+ }
+ return hints;
+}
+
async function portalRoutes(fastify) {
fastify.get("/portal/home", { preHandler: fastify.authenticate }, async (request, reply) => {
await ensureFeatureEnabled(fastify, request);
@@ -180,6 +200,8 @@ async function portalRoutes(fastify) {
onboardingCount,
offboardingCount,
assetReturnPendingCount,
+ activeEmployeeCount,
+ login7dCount,
] = await Promise.all([
fastify.prisma.workflowTask.count({ where: { status: "pending", OR: assigneeFilters } }),
fastify.prisma.reimbursementForm.count({ where: { OR: [{ owner_id: request.employee.id }, { submitter_id: request.employee.id }] } }),
@@ -225,6 +247,19 @@ async function portalRoutes(fastify) {
featureEnabled.employeeLifecycle && isManagerView
? fastify.prisma.assetCard.count({ where: { assignee: { is: { employment_status: "offboarding" } } } })
: Promise.resolve(0),
+ isManagerView ? fastify.prisma.employee.count({ where: { status: "active" } }) : Promise.resolve(0),
+ isManagerView
+ ? fastify.prisma.authSession.findMany({
+ where: {
+ revoked_at: null,
+ expires_at: { gt: new Date() },
+ last_seen_at: { gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
+ employee: { status: "active" },
+ },
+ select: { employee_id: true },
+ distinct: ["employee_id"],
+ }).then((rows) => rows.length)
+ : Promise.resolve(0),
]);
const todayPresence = featureEnabled.presenceBoard
@@ -236,15 +271,14 @@ async function portalRoutes(fastify) {
const teamWeeklySummary = featureEnabled.weeklyReport && isManagerView
? await (async () => {
- const [employeeCount, submittedCount, needsHelpCount] = await Promise.all([
- fastify.prisma.employee.count({ where: { status: "active" } }),
+ const [submittedCount, needsHelpCount] = await Promise.all([
fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), status: "submitted" } }),
fastify.prisma.weeklyReport.count({ where: { week_start: parseDateOnlyString(currentWeekStart), needs_help: true } }),
]);
return {
weekStart: currentWeekStart,
submittedCount,
- missingCount: Math.max(0, employeeCount - submittedCount),
+ missingCount: Math.max(0, activeEmployeeCount - submittedCount),
needsHelpCount,
};
})()
@@ -325,6 +359,33 @@ async function portalRoutes(fastify) {
cards.push({ key: "risk_flags", title: "待复核风险", value: riskCount, href: "/ap-operations", severity: "high" });
}
+ const adoptionSummary = isManagerView
+ ? (() => {
+ const presenceUpdatedCount = teamPresenceSummary ? teamPresenceSummary.total - teamPresenceSummary.unspecified : 0;
+ const weeklySubmittedCount = teamWeeklySummary?.submittedCount ?? 0;
+ return {
+ activeEmployeeCount,
+ login7dCount,
+ weeklySubmittedCount,
+ weeklyMissingCount: teamWeeklySummary?.missingCount ?? 0,
+ presenceUpdatedCount,
+ presenceMissingCount: teamPresenceSummary?.unspecified ?? 0,
+ announcementCount: announcementRows.length,
+ login7dRate: activeEmployeeCount > 0 ? Number(((login7dCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
+ weeklySubmissionRate: activeEmployeeCount > 0 ? Number(((weeklySubmittedCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
+ presenceCoverageRate: activeEmployeeCount > 0 ? Number(((presenceUpdatedCount / activeEmployeeCount) * 100).toFixed(1)) : 0,
+ hints: buildAdoptionHints({
+ activeEmployeeCount,
+ login7dCount,
+ weeklySummary: teamWeeklySummary,
+ teamPresenceSummary,
+ offboardingCount,
+ assetReturnPendingCount,
+ }),
+ };
+ })()
+ : null;
+
return sendData(reply, request, {
workspace,
greeting: { name: request.employee.name ?? request.employee.id },
@@ -370,6 +431,7 @@ async function portalRoutes(fastify) {
href: "/people",
}
: null,
+ adoptionSummary,
});
});
}