Преглед на файлове

docs: 规划师多类别认证 + 线下课复用活动管理 设计规格 (v2.1)

- 认证从单一规划师认证升级为多学习种类(类别)认证: 营养师/测评师等各自独立课程/课时/考核/证书
- 新增线下课程打卡: 复用主 activities 模块承载(发布/报名/海报二维码/可见范围), 通过 planner_offline_course_map 桥接到认证类别
- 落地 activity_checkins 通用签到记录, 修复 activities 原"签到审核形同虚设"旧患
- 课时池 = 线上(小鹅通) + 线下(活动签到) 按类别合并; 接单闸门改为类别感知 assertCertified(userId, categoryCode)
zxyj-web преди 3 седмици
родител
ревизия
cbf0808960
променени са 1 файла, в които са добавени 406 реда и са изтрити 0 реда
  1. 406 0
      docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md

+ 406 - 0
docs/superpowers/specs/2026-10-22-planner-learning-assessment-gate-design.md

@@ -0,0 +1,406 @@
+# 规划师在线学习 · 多类别认证 · 线下打卡 · 接单闸门 设计规格
+
+日期: 2026-10-22(v2:新增「学习种类/多类别认证」+「线下课程打卡」)
+状态: 📝 设计中(待评审)
+作者: AI 助理(基于需求与现有代码梳理)
+关联: `docs/superpowers/specs/2026-07-30-service-role-apply-design.md`(服务角色统一申请,已实施)
+
+---
+
+## 1. 需求拆解
+
+| 编号 | 需求原文要点 | 系统含义 |
+|------|--------------|----------|
+| R1 | 系统应**同时承载**规划师在线学习、考核功能 | 新增「规划师培训认证」模块,与现有规划师接单/服务模块并行 |
+| R2 | 学习视频**通过小鹅通管理** | 视频内容与播放托管在外部(小鹅通),本系统只消费"学习进度/课时"(线上渠道之一) |
+| R3 | 规划师**必须完成指定课时才能考核** | 课时达标是考核的硬前置条件(未达标不可进入考核) |
+| R4 | 考核通过后才能**接单、提供服务** | 在"身份审核通过"与"可服务"之间插入认证闸门;未认证通过,对应类别服务入口关闭 |
+| **R5** | **可设置不同学习种类**(如营养师、测评师…) | 认证从「单一规划师认证」升级为「**多类别认证**」:每类别独立课程、课时要求、考核、证书;规划师可持有多个类别认证 |
+| **R6** | **支持线下课程的打卡记录** | **复用现有「活动管理」模块**承载线下培训课(发布/报名/海报二维码/可见范围),线下签到课时与线上(小鹅通)课时**合并**计入该类别总课时;不新建独立课程表 |
+
+**职责切分(设计基线)**:
+- 线上学习视频与播放 = 小鹅通(外部);课时回传/手动同步 = 本系统。
+- 线下课程**复用活动管理 `activities` 模块**承载(建课/发布/报名/海报二维码/可见范围);现场签到记录落 `activity_checkins`,课时认定与本系统认证桥接 = 本系统。
+- 课程目录映射、课时统计(线上+线下合并)、考核、认证闸门、接单管控 = 本系统。
+- 考核**自建**,不依赖小鹅通或线下工具。
+
+---
+
+## 2. 现状与冲突点(Gap Analysis)
+
+| 现状(已实施) | 缺口 |
+|----------------|------|
+| `service_role_applications`:用户申请 → 管理员审核身份 → 通过即 `Guide.status="active"`(见 `ServiceRoleApplicationServiceImpl.approve()`,planner 分支直接 `setStatus("active")`) | 没有任何学习/考核环节;审核通过即可服务,与 R4 冲突 |
+| 现有 `TrainingPlan` 是规划师给**孩子**布置的训练方案,非规划师**自身**培训 | 缺少规划师自身的学习/考核载体 |
+| 全仓检索 `小鹅通/xiaoe`:**零命中** | 小鹅通接入为全新集成点,需设计回调/同步契约(仅限线上渠道) |
+| 管家有"停接/恢复接单"开关(`accept_new_family_binding`),规划师无对应认证级开关 | 规划师的"能否接单"目前仅靠 `Guide.status`,需扩展为**类别级认证维度** |
+| 无「学习种类/多类别」概念 | 现有模型是"一个规划师一张认证",需拆为"一个规划师 × N 个类别认证" |
+| 线下课载体选型 | **决定复用主 `activities` 模块**(发布/报名/海报二维码/可见范围成熟),不新建独立课程表;其签到记录层缺失(见下),需补 `activity_checkins` 并修复"签到审核形同虚设"旧患 |
+| `guide_activities` 是空壳 | 仅 9 字段 CRUD,无报名/签到/审核/二维码;**不复用**,避免重建且语义混淆 |
+| 主 `activities` 签到记录缺失 | `Activity` 仅有 `requireCheckinReview`/`checkinPoints` 字段,无真实签到记录表(此前调研标记 check-in 从不读取);本方案一并补 `activity_checkins` 落地真实签到 |
+
+**核心改造点**:
+1. planner 审核通过后的 `status="active"` 拆为"身份已审 + 待认证 + 学习中 + 考核中 + 已认证(按类别)",仅持有对应类别认证方可提供该类别服务。
+2. 认证主表由"每用户一行"改为"每用户每类别一行"。
+3. 课时池 = 线上(小鹅通)课时 + 线下(活动签到)课时,按类别汇总。
+
+---
+
+## 3. 概念模型:学习种类(认证类别)
+
+- **学习种类 = 认证类别(certification category)**,由管理端可配置(CRUD),非硬编码。
+- 首批候选类别(可扩展):
+
+| code | 名称 | 是否基础类别 | 说明 |
+|------|------|--------------|------|
+| `planner` | 成长规划师 | ✅ 是 | 规划师基础身份,默认必学必考;获得后即 `Guide.status=active` |
+| `nutritionist` | 营养师 | 否 | 营养方案类服务的前置认证 |
+| `assessor` | 测评师 | 否 | 测评录入/解读类服务的前置认证 |
+
+- 每个类别独立配置:`required_hours`(总课时,线上+线下)、课程包、考核试卷。
+- 一名规划师可持有 1~N 个类别认证;**某类服务需持有对应类别认证**(`planner` 类别认证是接单基础,其余类别为增值服务前置)。
+
+---
+
+## 4. 规划师生命周期状态机(按类别)
+
+```
+         申请身份(service_role_applications)
+                    │  管理员审核身份
+                    ▼
+           identity_approved  (Guide.status = pending_cert)
+                    │  自动建「每类别」planner_certifications(均 pending_learning)
+                    ▼
+   ┌─────────────────────────────────────────────┐
+   │  每类别独立流转(以 nutritionist 为例):        │
+   │                                               │
+   │  pending_learning                             │
+   │   (线上小鹅通课时 + 线下活动签到课时 累计)      │
+   │        │  completed_hours >= required_hours   │
+   │        ▼                                      │
+   │  learning_completed  (开放该类别考核)          │
+   │        │  考核通过 score >= pass_score         │
+   │        ▼                                      │
+   │  certified  (持有该类别证书,可提供该类别服务)  │
+   └─────────────────────────────────────────────┘
+                    │  至少持有基础类别(planner) certified
+                    ▼
+           Guide.status = active  (可接单基础服务)
+                    │
+   增值类别(nutritionist/assessor) certified ──▶ 可接对应增值服务
+
+   异常/回流路径(每类别独立):
+   learning_completed ──考核未过──▶ exam_failed ──可重考──▶ learning_completed
+   exam_failed 累计次数超限 ──▶ 管理员复核 / 退回 pending_learning
+   certified(若启用有效期)过期 ──▶ suspended ──复训达标后──▶ certified
+   某类别 suspended 不影响其他类别 certified 状态
+```
+
+> `Guide.status` 取值:`pending_cert`(身份已审、尚无任何类别认证)/ `active`(至少基础类别 certified)/ `suspended`(如启用有效期且全部过期)。
+> `planner_certifications.status`(每用户每类别):`pending_cert` / `learning_completed` / `certified` / `suspended`。
+> `learning_status`:`pending_learning` / `learning_completed`;`exam_status`:`none` / `exam_failed` / `exam_passed`。
+
+---
+
+## 5. 小鹅通集成方案(线上渠道,关键设计决策)
+
+### 5.1 推荐模式:H5/小程序 SDK 嵌入 + 开放平台 Webhook 回传
+- **播放侧**:规划师端以 WebView 嵌入小鹅通课程 H5,或调用小鹅通小程序 SDK;视频与版权在小鹅通。课程须归属某「学习种类」(`planner_courses.category_id`)。
+- **回传侧**:注册小鹅通开放平台回调(消息推送),监听"课时完成 / 课程完成"事件。
+- **本系统只存**:课程目录映射、每用户每课程进度、总完成课时;不存视频。
+
+### 5.2 鉴权与契约
+- 配置项(`application.yml`):`xiaoe.app-id`、`xiaoe.app-secret`、`xiaoe.token`、`xiaoe.callback-path`。
+- `XiaoeWebhookController`:校验回调签名 → 解析事件 → 写入 `planner_course_progress` → 重算该类别 `planner_certifications.completed_hours`(仅累加线上部分)→ 达标则 `pending_learning → learning_completed`。
+- 幂等:以(user_id, 小鹅通 resource_id, 事件ID)去重。
+
+### 5.3 无凭证期 Fallback(保证可先开发、可演示)
+- `XiaoeAdapter` 接口,两种实现:`XiaoeWebhookAdapter`(真实)与 `XiaoeManualAdapter`(沙箱)。
+- 管理端"手动同步课时"入口:可录入/批量导入每规划师每课程完成课时。
+- 上线切换:配置开关 `xiaoe.mode = webhook | manual`,无需改代码。
+
+---
+
+## 6. 线下课程与打卡(复用活动管理模块,R6)
+
+> **选型决定(已与用户确认)**:线下培训课**不复用 `guide_activities`(空壳)**,而是**复用主 `activities` 模块**作为"事件容器",通过一张映射表桥接到认证。主 `activities` 已具备发布/报名/海报二维码/可见范围,可省去重建事件 CRUD;但其签到记录层缺失,本方案一并补 `activity_checkins` 并修复"签到审核形同虚设"的旧患。
+
+### 6.1 建课(管理端,复用 activities)
+- 在现有 `activities` 模块创建活动(标题/地点/时间/海报二维码),并设 `visibleScope='role'`、`visibleTo='["teacher"]'`,使其仅对规划师可见。
+- 通过 `planner_offline_course_map` 把该活动**映射**到某一学习种类:`(activity_id, category_id, duration_hours)`——即"此活动签到后给该类别计多少课时"。
+- 一门活动可映射一个类别(v1);活动本身仍是通用活动,培训语义只存在于映射表,不污染 `activities` 主表。
+
+### 6.2 报名与签到(规划师端 / 管理端,复用 activities)
+- **报名**:规划师(role=teacher,作为 `activity_registrations.userId`)报名参加该培训活动——复用现有报名链路。
+- **扫码签到(主)**:现场扫活动签到二维码(编码 activity_id + 签名 token)→ `POST /api/planner/training-activities/check-in {qr_token}` → 校验 → 写 `activity_checkins`(check_in_type='qr',user_id=规划师)→ 桥接发放课时。
+- **管理端手动补录(fallback)**:`check_in_type='admin'`、`check_in_by=管理员`,补漏扫。
+- **GPS 签到(可选,见 D7)**:预留 `check_in_type='gps'`,v1 默认不启用。
+
+### 6.3 课时桥接与合并
+- 签到成功后,桥接逻辑按 `planner_offline_course_map.duration_hours` 给该规划师对应类别的 `planner_certifications.completed_hours` 累加(幂等)。
+- 某类别 `planner_certifications.completed_hours` = Σ(该类别线上 `planner_course_progress.completed_hours`) + Σ(该类别 `activity_checkins` 经映射表发放的课时)。
+- 任一部分达标即可翻转 `learning_completed`;线下活动可独立撑满课时(不强制线上,见 D8)。
+- 幂等:`activity_checkins` 设 `uk_activity_user (activity_id, user_id)`,一人一活动一次签到;重复签到不重复计课时。
+
+---
+
+## 7. 数据模型(DDL 草案 v2)
+
+```sql
+-- ① 学习种类 / 认证类别(管理端可配置)
+CREATE TABLE `planner_cert_categories` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `code` varchar(32) NOT NULL COMMENT '类别编码: planner/nutritionist/assessor ...',
+  `name` varchar(64) NOT NULL COMMENT '类别名称',
+  `description` varchar(255) DEFAULT '',
+  `required_hours` decimal(6,2) NOT NULL DEFAULT 0 COMMENT '该类别要求总课时(线上+线下)',
+  `is_base` tinyint(1) DEFAULT 0 COMMENT '1=基础类别(获得即 Guide.status=active)',
+  `order_index` int(11) DEFAULT 0,
+  `status` tinyint(4) DEFAULT 1 COMMENT '1启用 0停用',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_code` (`code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师学习种类/认证类别';
+
+-- ② 线上课程目录(映射小鹅通课程/专栏)
+CREATE TABLE `planner_courses` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `xiaoe_resource_id` varchar(64) DEFAULT '' COMMENT '小鹅通课程/商品ID',
+  `title` varchar(120) NOT NULL,
+  `required_hours` decimal(6,2) NOT NULL DEFAULT 0 COMMENT '该课程要求课时',
+  `order_index` int(11) DEFAULT 0,
+  `is_required` tinyint(1) DEFAULT 1 COMMENT '是否必修',
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_category` (`category_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程目录(小鹅通)';
+
+-- ③ 每用户每线上课程学习进度(来自小鹅通回传/手动)
+CREATE TABLE `planner_course_progress` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `course_id` int(11) NOT NULL,
+  `category_id` int(11) NOT NULL,
+  `xiaoe_resource_id` varchar(64) DEFAULT '',
+  `progress_percent` int(11) DEFAULT 0,
+  `completed_hours` decimal(6,2) DEFAULT 0,
+  `completed` tinyint(1) DEFAULT 0,
+  `last_synced_at` datetime DEFAULT NULL,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_course` (`user_id`,`course_id`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师线上课程学习进度';
+
+-- ④ 活动→认证类别映射(桥接表:线下培训课复用 activities 模块)
+CREATE TABLE `planner_offline_course_map` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `activity_id` bigint NOT NULL COMMENT '关联 activities.id(线下培训活动)',
+  `category_id` int(11) NOT NULL COMMENT '关联 planner_cert_categories.id',
+  `duration_hours` decimal(6,2) DEFAULT 0 COMMENT '该活动签到后计入的课时',
+  `is_required` tinyint(1) DEFAULT 0 COMMENT '是否必修环节',
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity` (`activity_id`),
+  KEY `idx_category` (`category_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='线下培训活动↔认证类别映射(桥接)';
+
+-- ⑤ 活动签到记录(复用 activities 模块,一并修复"签到审核形同虚设"旧患)
+CREATE TABLE `activity_checkins` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `activity_id` bigint NOT NULL,
+  `user_id` int(11) NOT NULL COMMENT '签到人(规划师=userId)',
+  `registration_id` bigint DEFAULT NULL COMMENT '关联报名记录',
+  `check_in_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '签到时间',
+  `check_in_type` varchar(16) DEFAULT 'qr' COMMENT 'qr扫码/admin手动/gps',
+  `check_in_by` int(11) DEFAULT NULL COMMENT 'admin手动补录人',
+  `review_status` varchar(16) DEFAULT 'approved' COMMENT 'approved/pending/rejected(对应原 requireCheckinReview)',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity_user` (`activity_id`,`user_id`),
+  KEY `idx_user` (`user_id`),
+  KEY `idx_activity` (`activity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='活动签到记录(通用,供培训课时桥接)';
+
+-- ⑥ 规划师认证主表(每用户每类别一行)
+CREATE TABLE `planner_certifications` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `guide_id` bigint DEFAULT NULL,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `category_code` varchar(32) DEFAULT '' COMMENT '冗余类别编码,便于查询',
+  `required_hours` decimal(6,2) DEFAULT 0 COMMENT '该类别总要求课时',
+  `completed_hours` decimal(6,2) DEFAULT 0 COMMENT '已完成课时(线上+线下汇总)',
+  `learning_status` varchar(20) DEFAULT 'pending_learning' COMMENT 'pending_learning/learning_completed',
+  `exam_status` varchar(20) DEFAULT 'none' COMMENT 'none/exam_failed/exam_passed',
+  `exam_score` int(11) DEFAULT NULL,
+  `exam_attempts` int(11) DEFAULT 0,
+  `cert_no` varchar(64) DEFAULT '' COMMENT '证书编号',
+  `certified_at` datetime DEFAULT NULL,
+  `expire_at` datetime DEFAULT NULL COMMENT 'NULL=不过期',
+  `status` varchar(20) DEFAULT 'pending_cert' COMMENT 'pending_cert/learning_completed/certified/suspended',
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_category` (`user_id`,`category_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师每类别培训认证主表';
+
+-- ⑦ 考核试卷(每类别可有多套)
+CREATE TABLE `planner_exam_papers` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `category_id` int(11) NOT NULL COMMENT '所属学习种类',
+  `title` varchar(120) NOT NULL,
+  `pass_score` int(11) NOT NULL DEFAULT 60,
+  `duration_minutes` int(11) DEFAULT 30,
+  `question_count` int(11) DEFAULT 0,
+  `status` tinyint(4) DEFAULT 1,
+  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `idx_category` (`category_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核试卷';
+
+-- ⑧ 考核题库
+CREATE TABLE `planner_exam_questions` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `paper_id` int(11) NOT NULL,
+  `question` text NOT NULL,
+  `options` text COMMENT 'JSON数组',
+  `answer` varchar(10) COMMENT '正确选项',
+  `score` int(11) DEFAULT 1,
+  `order_index` int(11) DEFAULT 0,
+  PRIMARY KEY (`id`),
+  KEY `idx_paper` (`paper_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考核题库';
+
+-- ⑨ 考核作答记录
+CREATE TABLE `planner_exam_attempts` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `user_id` int(11) NOT NULL,
+  `category_id` int(11) NOT NULL,
+  `paper_id` int(11) NOT NULL,
+  `score` int(11) DEFAULT 0,
+  `passed` tinyint(1) DEFAULT 0,
+  `answers` text COMMENT 'JSON 作答明细',
+  `started_at` datetime DEFAULT CURRENT_TIMESTAMP,
+  `submitted_at` datetime DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  KEY `idx_user` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规划师考核作答记录';
+```
+
+> `Guide` 表无需新增列:`planner_certifications` 承载类别级认证维度;`Guide.status` 在基础类别 `certified` 时由 `pending_cert` 翻为 `active`。
+
+---
+
+## 8. 考核(本系统自建,按类别)
+
+- **管理端**:试卷/题库维护(每类别独立,增删题、设及格线、限时);支持多套试卷。
+- **规划师端-考核入口**:仅当该类别 `planner_certifications.learning_status = learning_completed` 才开放;否则提示"请先完成必修课时(当前 X/Y 课时,含线上+线下)"。
+- **作答与判分**:限时作答 → 提交即判分 → 写入 `exam_score`/`exam_passed`/`planner_exam_attempts` → 通过则 `exam_status=exam_passed`、触发 `certified` 状态流转、生成 `cert_no`。基础类别通过时同步翻 `Guide.status=active`。
+- **重考**:未过可重考,`exam_attempts` 累加;连续超限(可配,如 3 次)转管理员复核。
+
+---
+
+## 9. 接单 / 服务闸门(类别感知,核心拦截)
+
+**中央守卫**:`PlannerCertificationGuard.assertCertified(userId, categoryCode)` —— 查询该类别 `planner_certifications.status == certified`(且 `Guide.status == active`)方可放行,否则抛 `Result.error("规划师未完成【类别名】培训认证,暂不可提供该类服务")`。
+
+**服务动作 → 所需类别映射**(代码级 enum/常量,后续可下沉为管理端配置表):
+
+| 服务动作 | 现有入口(参考 guide-system-flow) | 所需类别 | 拦截 |
+|----------|----------------------------------|----------|------|
+| 创建/上架服务套餐(基础) | `GuidePackageController.create/update-status` | `planner` | ✅ |
+| 生成家庭绑定邀请(开始服务) | `GuideFamilyController.bind/invite` | `planner` | ✅ |
+| 激活成长方案(下发任务) | `PlanReviewController.activate` | `planner` | ✅ |
+| 营养方案 / 饮食建议 | `GuidePackageController` 营养类目 / `GuideRecordController` 营养记录 | `nutritionist` | ✅ |
+| 测评录入 / 解读 | `GuideAssessmentController` | `assessor` | ✅ |
+| 接单(订单/咨询响应) | `GuideOrderController`、`TeacherMessageController.reply` | `planner`(基础)+ 具体增值类别按服务内容判定 | ✅ |
+
+**两端一致**:
+- 规划师端"服务"类入口在缺少对应类别认证时**置灰**并提示认证进度(含线上+线下课时);
+- 服务端守卫**拒绝**非认证请求(防绕过);
+- 管理端"规划师认证看板"按类别展示:课时进度(线上/线下拆分)、考核分、证书号、有效期。
+
+---
+
+## 10. 接口清单(草案 v2)
+
+### 规划师端
+- `POST /api/planner/certification/my` → 我的全部类别认证状态(课时、线上/线下拆分、是否可考、证书)
+- `POST /api/planner/courses` → 某类别线上课程列表(含小鹅通跳转/H5 链接)
+- `POST /api/planner/training-activities` → 可报名/签到的培训活动列表(复用 activities,过滤映射表+规划师可见)
+- `POST /api/planner/training-activities/check-in` → 扫码/手动签到(body: qr_token 或 activity_id+admin)→ 桥接发放课时
+- `POST /api/planner/exam/start` → 开始某类别考核(前置校验课时达标)
+- `POST /api/planner/exam/submit` → 提交判分
+- `POST /api/planner/exam/result` → 考核结果
+
+### 管理端
+- `POST /api/admin/planner-cert-categories/*` 学习种类 CRUD(含 required_hours、is_base)
+- `POST /api/admin/planner-courses/*` 线上课程目录 CRUD(挂类别)
+- `POST /api/admin/planner-offline-course-map/*` 培训活动↔类别映射 CRUD(在 activities 建课后关联类别与计课时)
+- `POST /api/admin/activity-checkins/*` 签到记录查询 / 手动补录
+- `POST /api/admin/planner-exam/*` 试卷/题库 CRUD(挂类别)
+- `POST /api/admin/planner-certifications/sync-hours` 手动同步线上课时(fallback)
+- `POST /api/admin/planner-certifications/list` 认证看板(按类别)
+
+### 小鹅通回调
+- `POST /api/xiaoe/webhook` 线上学习进度/课程完成事件(签名校验、幂等)
+
+---
+
+## 11. 前端页面(草案)
+
+- 规划师端:新增「培训认证」中心页(按类别 Tab:课时进度条【线上+线下】+ 线上课程入口 + 线下课扫码打卡 + 考核入口)。
+- 管理端:学习种类管理、线上课程管理、线下课程/打卡管理、考核题库管理、规划师认证看板(按类别)。
+- 复用现有 `pages/profile/service-role-apply` 之后的流向,新增 `planner` 子包页面。
+
+---
+
+## 12. 实施顺序(建议)
+
+1. 数据库迁移(9 张表 + 类别种子数据 `planner_cert_categories` 首批 3 类)+ `DatabaseInitializer` 迁移。
+2. 后端实体/Mapper/Service:`PlannerCertCategoryService`、`PlannerCourseService`、`OfflineCourseService`(建课+打卡)、`PlannerExamService`、`XiaoeAdapter`(webhook+manual)。
+3. `ServiceRoleApplicationServiceImpl` planner 分支改造:审核通过 → 建 Guide(`pending_cert`)+ 建**每类别** `planner_certifications`。
+4. `PlannerCertificationGuard.assertCertified(userId, categoryCode)` + 服务入口注入(基础类别 + 增值类别映射)。
+5. 小鹅通 Webhook 接入 + 线上课时手动同步 fallback。
+6. 线下培训课:复用 `activities` 建课/报名/海报二维码(可见范围限定规划师)+ 落地 `activity_checkins` 真实签到(修复旧患)+ `planner_offline_course_map` 桥接 + 签到→课时入账。
+7. 规划师端「培训认证」中心页(类别 Tab + 线上/线下课时 + 打卡 + 考核)。
+8. 管理端:类别/课程/线下课/题库/认证看板。
+9. E2E:身份审核通过 → 课时未达标禁考 → 线上+线下课时达标 → 考核通过 → 对应类别接单解禁。
+
+---
+
+## 13. 待确认决策(需你拍板)
+
+| # | 决策点 | 本设计默认假设 | 影响 |
+|---|--------|----------------|------|
+| D1 | 小鹅通接入深度 | 开放平台 Webhook 回传 + 手动同步 fallback(开发期先 manual) | 决定是否需要申请小鹅通开放平台权限 |
+| D2 | **首批学习种类范围 + 基础类别是否必须** | 首批 3 类(planner 基础 / nutritionist / assessor);`planner` 为必须基础,其余为增值 | 决定类别种子数据与接单映射表;新增类别走管理端 CRUD |
+| D3 | 考核防作弊强度 | 初版:限时 + 固定及格线,不做随机/人脸 | 影响题库规模与前端复杂度 |
+| D4 | 证书有效期 / 复训 | 初版:`expire_at = NULL`(不过期) | 若需复训,`suspended` 回流路径要启用 |
+| D5 | "接单"具体动作集合 | 按第 9 节映射表统一闸门(基础类别 + 增值类别按服务内容) | 若"接单"仅指订单类,可缩小拦截范围 |
+| D6 | 现有已通过的规划师如何处理 | 存量 Guide 按"已持有基础类别 certified/active"补登(其余类别 pending_learning),不追溯补训 | 避免存量规划师被误封 |
+| **D7** | **线下课载体与打卡方式(已定)** | 载体**复用 `activities`** + `planner_offline_course_map` 桥接(不建独立课程表、不复用 guide_activities);打卡主用**扫码签到**(活动签到二维码),管理端手动补录为 fallback;GPS 签到 v1 不启用 | 复用一并修复 activities 原"签到审核形同虚设"缺陷 |
+| **D8** | **线下课时是否可完全替代线上** | 默认**可合并撑满**(不强制线上最低占比),线下课即可独立达标 | 若要求"线上必修保底",需在类别上增 `min_online_hours` 约束 |
+
+---
+
+## 14. 风险与边界
+
+- **外部依赖**:小鹅通 Webhook 稳定性/延迟 → 以 `last_synced_at` + 手动同步兜底,不直接阻塞规划师学习。
+- **线下防作弊**:扫码二维码需带有效期与一次性/课程绑定,避免截图代打卡;管理端手动补录留痕(`check_in_by`)。
+- **课时口径一致性**:线上、线下、合计三者在看板与闸门须同源计算(`completed_hours` 单一汇总字段),避免双写不一致。
+- **合规**:考核不得承诺"包过/保收入";课程与证书表述需符合现有《对外表达禁用词表》口径(宝妈+儿童敏感)。
+- **幂等**:Webhook 重放、手动同步、重复打卡均需去重(见各表唯一键)。
+- **存量兼容**:D6 处理不当会误封已服务规划师,需灰度上线。