# 学邦现有问题 132 项全量解决 Implementation Plan

**Goal:** 在一个完整发布范围内关闭补充 PRD 的全部 132 项问题；逐项保留业务结果和验收证据，但只实现最新主分支尚未具备的最小差异，不以阶段或优先级裁剪范围，也不为凑齐层级而新增数据库、接口或测试。

**Architecture:** 基于现有 Java 模块化单体和 Vue Vben 管理端，优先复用已有 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、审计、幂等和导入导出能力；只有领域真值、状态机、结算台账或现有契约无法承载时才新增对象。开发按依赖顺序分阶段，最终以同一发布候选完成全量联调、数据迁移、必要回归和 UAT。

**Tech Stack:** Java 17、Spring Boot 3.3.7、Spring JDBC、PostgreSQL、Flyway、Redis、RabbitMQ、MinIO、OpenSearch、Vue 3、TypeScript、Vben、Ant Design Vue、Vitest、Playwright、Maven、Docker Compose。

**Locked baseline:** `origin/main@f4e42c83ea28838bbb857074cf62a4f7c0a2f581`（提交时间 2026-07-29T00:07:41+08:00）；生成计划时 `HEAD == origin/main`，最新 Flyway 为 `V89__protect_platform_group_root_status.sql`。

## Global Constraints

1. 132 项是同一发布范围。下文 Stage 只表达技术依赖和合并顺序，不代表可删减范围，也不允许只交付某个优先级。
2. 开工必须从 `origin/main@f4e42c83ea28838bbb857074cf62a4f7c0a2f581` 或更新后的最新主分支创建开发工作树；若 `origin/main` 已推进，先重跑本文的基线审计和接口生成器，再更新锁定 SHA，禁止在旧分支直接开发。
3. 本仓库是单一 Git 仓库；`SourceCode/dinuo-admin-api`、`SourceCode/dinuo-admin-vben`、`SourceCode/dinuo-infra` 是本计划的三个交付子项目。`SourceCode/admin_v2` 是上游模板，`SourceCode/dinuo-admin-web` 是早期管理端，不作为本次业务实现目标。
4. 专用业务闭环必须进入领域 Controller/Service/Models；通用 CRUD 只承载简单主数据，不得用 `Map<String,Object>` 或无约束 JSON 代替结算规则、状态机和财务真值。
5. 所有跨模块关系使用稳定 UUID；名称、手机号、课程名只能展示或候选检索。迁移时保留 `legacySystem + legacyId`，不得用旧 ID 作为新外键。
6. 所有查询、Lookup、详情、导出、异步任务和缓存统一叠加 tenant、组织、校区、业务线、学段和字段权限；缓存键必须包含 `scopeHash`。
7. 所有写接口校验 `expectedVersion`，使用 `Idempotency-Key` 或请求体 `idempotencyKey`；重复请求返回首次结果，载荷不同则返回 409。
8. 金额使用 `numeric(18,2)`/`BigDecimal`，比例使用 `numeric(18,6)`，时间存 UTC `OffsetDateTime`，自然周期按校区时区解析为闭开区间。
9. 合同、课消、退款、课酬、奖励、业绩和工资台账采用追加版本或正负差异，不覆盖已结算历史；关账后只能生成追补。
10. 大列表强制后端分页；大导出和三个月以上查询走异步任务。页面、导出和汇总必须共享 `filterHash + scopeHash + metricVersion`。
11. 只有跨模块、异步或财务副作用链路使用事务 Outbox、消费者幂等、重试和死信；普通查询、同步主数据维护、字段标签和页面布局不写 Outbox。
12. 只有真实 Schema、约束、索引、历史映射或领域台账缺口才创建 Flyway；候选迁移名用于依赖排序，不要求每个问题域都产生迁移，更不允许空迁移。
13. 每项先做最新主分支差异确认并标记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP，再做最小改动。已解决项可以零代码关闭，待验收项必须先验收后开发。
14. 每项必须有可追溯验收证据，但迁移、OpenAPI、JUnit、Vitest、Playwright、对账只在对应改动或风险存在时必填；禁止为满足证据字段制造无效实现。

## 0. 最新主分支基线与开工方式

### 0.1 本次复核结果

| 检查项 | 最新主分支结果 | 计划约束 |
| --- | --- | --- |
| Git 基线 | `HEAD=f4e42c83ea28838bbb857074cf62a4f7c0a2f581`，`origin/main=f4e42c83ea28838bbb857074cf62a4f7c0a2f581` | 当前一致；本文按该 SHA 生成 |
| 132 项审计基线差异 | 原核对报告最后提交 `91075f6ee677667fac4b408049e30c8ea6b4beb2`；到最新主分支的 Java、Vue src、Flyway 运行时代码差异为 `0` | 最新合并未改变 132 项对应业务实现；接口存在性已按最新 OpenAPI 重新判断 |
| Flyway | 最新 `V89__protect_platform_group_root_status.sql` | 实施时从最新可用版本分配；候选 V90-V111 仅表示依赖分组，无 V112 研发证据表 |
| OpenAPI | `863` 个 path、`1148` 个 operation | Controller 注解是事实源，生成产物不得手工维护 |
| 页面清单 | `232` 个 pageCode | 本文每项页面编码均已在最新 manifest 校验存在 |
| 正式后端 | `SourceCode/dinuo-admin-api` | 全部专用接口、迁移、事件、台账和测试落在此项目 |
| 正式管理端 | `SourceCode/dinuo-admin-vben` | 全部 PC 页面、API client、契约测试和 Playwright 落在此项目 |
| 发布校验 | `SourceCode/dinuo-infra/validate-admin-stack.sh` | 最终加入 132 项覆盖审计并执行全栈门禁 |

### 0.2 从最新主分支创建独立工作树

```bash
cd /Users/ethan/Project/Dinuo
git fetch origin main --prune
git rev-parse origin/main
test "$(git rev-parse origin/main)" = "f4e42c83ea28838bbb857074cf62a4f7c0a2f581"
git worktree add ../Dinuo-xuebang-132 -b zhaocq/xuebang-132-full-closure origin/main
cd ../Dinuo-xuebang-132
git status --short --branch
```

若开工时主分支 SHA 已变化，不能继续执行上述固定 SHA 断言；应先在新 SHA 上重跑第 0.3 节，重新核对迁移、页面、Controller、OpenAPI 和本计划的新增/扩展接口分类。

### 0.3 主分支基线门禁

```bash
node scripts/generate_admin_api_catalog.mjs --check
node scripts/generate_admin_openapi.mjs --check
node scripts/audit_admin_api_contract_consistency.mjs --check
```

若该 SHA 已有可信的后端、前端、类型和构建 CI 绿灯，直接复用，不重复运行全量测试；缺少基线证据时才在开工前运行一次现有全量门禁。基线失败时先区分主分支既有失败与本计划问题并形成记录。

## 1. 目标工程结构与迁移顺序

下表中的 Flyway 是候选合并文件：只有当前 Schema 差异确认确需落库时才创建；同一依赖组优先合并，实际编号以开工时最新主分支为准。

| Stage | 问题域 | 问题编号 | Flyway | 后端主文件 | 前端主文件 |
| --- | --- | --- | --- | --- | --- |
| 1 | 数据加载策略 | LOAD-01、LOAD-02、LOAD-03、LOAD-04、LOAD-05 | 候选 `V90__xuebang_query_load_policy_and_saved_queries.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java` | `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts` |
| 1 | 数据查询提效 | QUERY-01、QUERY-02、QUERY-03 | 候选 `V91__xuebang_query_performance_snapshots.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java` | `SourceCode/dinuo-admin-vben/src/components/business/BusinessPeriodPicker.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts` |
| 1 | 列表数据与汇总 | LIST-01、LIST-02、LIST-03 | 候选 `V92__xuebang_list_filter_summary_contract.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java` | `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue` |
| 1 | 重要数据 ID | ID-01、ID-02、ID-03、ID-04、ID-05、ID-06、ID-07、ID-08、ID-09 | 候选 `V93__xuebang_stable_id_projection_and_legacy_mapping.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java` | `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts` |
| 2 | 课程设置 | COURSE-01、COURSE-02 | 候选 `V94__xuebang_course_classification_and_pricing_precision.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 2 | 常用参数 | PARAM-01 | 候选 `V95__xuebang_custom_field_option_lifecycle.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java` | `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 2 | 员工与审批 | APPROVAL-01、APPROVAL-02、APPROVAL-03、APPROVAL-04 | 候选 `V96__xuebang_approval_reapply_and_employment_history.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 2 | 权限与目标 | PERM-01、PERM-02、PERM-03、PERM-04、PERM-05、TARGET-01、TARGET-02、TARGET-03、TARGET-04 | 候选 `V97__xuebang_scope_and_target_governance.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java` | `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts` |
| 3 | 客户管理 | CRM-01、CRM-02、CRM-03、CRM-04、CRM-05、CRM-06、CRM-07 | 候选 `V98__xuebang_crm_and_student_operations.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 3 | 学员管理 | STU-01、STU-02、STU-03、STU-04、STU-05 | 候选 `V98__xuebang_crm_and_student_operations.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 4 | 优惠管理 | DISC-01、DISC-02、DISC-03、DISC-04、DISC-05、DISC-06、DISC-07 | 候选 `V99__xuebang_discount_rule_engine_v2.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue` |
| 4 | 合同管理 | CON-01、CON-02、CON-03、CON-04、CON-05、CON-06、CON-07、CON-08 | 候选 `V100__xuebang_contract_item_and_performance_allocation.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 4 | 线上商城 | MALL-01、MALL-02、MALL-03 | 候选 `V101__xuebang_tuition_account_and_mall_quote.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts` |
| 5 | 物品管理 | ASSET-01、ASSET-02 | 候选 `V102__xuebang_asset_export_tasks.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/assetAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 5 | 教师课时费 | PAY-01、PAY-02、PAY-03、PAY-04、PAY-05、PAY-06、PAY-07、PAY-08、PAY-09、PAY-10、PAY-11 | 候选 `V103__xuebang_teacher_compensation_engine.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java` | `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 5 | 业绩核算 | PERF-01、PERF-02、PERF-03、PERF-04、PERF-05、PERF-06 | 候选 `V104__xuebang_performance_settlement_engine.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts` |
| 5 | 过渡奖励 | TRANS-01、TRANS-02、TRANS-03、TRANS-04、TRANS-05、TRANS-06、TRANS-07、TRANS-08、TRANS-09、TRANS-10、TRANS-11、TRANS-12、TRANS-13、TRANS-14、TRANS-15 | 候选 `V105__xuebang_transition_reward_engine.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java` | `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 5 | 退费扣款 | REFUND-01、REFUND-02、REFUND-03、REFUND-04、REFUND-05、REFUND-06、REFUND-07、REFUND-08 | 候选 `V106__xuebang_refund_settlement_engine.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 6 | 在读学员 | ACTIVE-01、ACTIVE-02 | 候选 `V107__xuebang_active_student_metric.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java` | `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts` |
| 6 | 人力反馈补充 | HRF-01、HRF-02、HRF-03 | 候选 `V108__xuebang_hr_migration_and_workload_projection.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java` | `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts` |
| 6 | 报表模块 | RPT-01、RPT-02、RPT-03、RPT-04、RPT-05、RPT-06 | 候选 `V109__xuebang_metric_and_report_governance.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java` | `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts` |
| 7 | 优化建议 | OPT-01、OPT-02、OPT-03、OPT-04、OPT-05、OPT-06、OPT-07 | 候选 `V110__xuebang_bulk_change_and_fact_snapshot.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java` | `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue` |
| 7 | 已发现缺陷 | BUG-01、BUG-02、BUG-03、BUG-04、BUG-05、BUG-06 | 候选 `V111__xuebang_known_defect_regression_facts.sql`（按需） | `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java` | `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue` |
| 8 | 132 项交付证据与发布门禁 | 全部编号 | 无生产库迁移 | `scripts/audit_xuebang_132_coverage.mjs` | `docs/01阶段技术开发/学邦132项开发进度.json` |

### 1.1 依赖阶段

| Stage | 必须完成的能力 | 阶段出口 |
| --- | --- | --- |
| 1 | LOAD → QUERY → LIST → ID | 统一查询/周期/分页/汇总/稳定 ID 合同可被后续模块复用 |
| 2 | COURSE → PARAM → APPROVAL → PERM/TARGET | 课程分类、字段参数、审批重提、数据范围和目标模型可执行 |
| 3 | CRM → STU | 客户、家庭、学员、归属、时间线和 Student 360 使用稳定 ID |
| 4 | DISC → CON → MALL | 优惠规则、合同课程项、报价、余额结转和业绩分配形成闭环 |
| 5 | ASSET；PAY → PERF → TRANS → REFUND | 课酬、业绩、奖励、退款及工资追补按台账/事件闭环 |
| 6 | ACTIVE → HRF → RPT | 指标公式、迁移、负荷预测和经营报表只读已结算事实 |
| 7 | OPT → BUG | 批量能力、快照、导出和已知缺陷全部回归 |
| 8 | 全量联调、迁移演练、性能、对账、UAT、发布 | 132/132 证据齐全且全栈门禁通过 |

## 2. 统一接口、数据与测试合同

### 2.1 HTTP 合同

- 所有接口返回现有 `ApiResponse<T>(code,message,requestId,data)`；成功码沿用项目约定，失败由 `GlobalExceptionHandler` 输出。
- 列表统一返回 `PageResult<T>` 并扩展可选 `summary`、`summaryVersion`、`filterHash`、`scopeHash`；`summary` 计算当前全部筛选结果而非当前页。
- 必需请求头：`Authorization`；写请求另带 `Idempotency-Key`；链路统一生成/透传 `X-Request-Id`。租户和数据范围从登录态解析，不接受客户端伪造。
- 版本冲突返回 409；校验失败 400；条件不足、跨度超限或状态不允许返回 422；越权返回 403。错误详情必须包含稳定业务错误码和可定位字段。
- OpenAPI 由 Controller/Models 生成。每新增或修改接口后依次运行三个生成/审计脚本，并提交生成后的运行态 OpenAPI、文档 OpenAPI、接口目录和契约审计报告。

### 2.2 领域事件合同

```text
eventId: UUID
eventType: CONTRACT_SEALED | PAYMENT_POSTED | CONSUMPTION_POSTED | REFUND_COMPLETED | ...
aggregateType / aggregateId / aggregateVersion
tenantId / campusId / businessLineId / schoolStage
occurredAt / recordedAt / actorId
idempotencyKey / correlationId / causationId
payloadVersion / payload / beforeSnapshot / afterSnapshot
```

本合同只适用于跨模块、异步或财务副作用链路。生产事件和业务事务同库提交；消费者以 `eventId + consumerName` 唯一，失败可重试并进入死信。普通同步 CRUD、查询、标签和布局不发布事件。

### 2.3 风险选测矩阵

| 改动类型 | 最小必要验证 | 不需要的默认测试 |
| --- | --- | --- |
| 已解决、只复用现有能力 | 核对现有测试；缺关键边界时扩展一个现有用例 | 新建整域 JUnit/Vitest/Playwright |
| 待业务验收、布局或文案 | 真实数据 UAT；失败后补最接近缺陷层的一条回归 | 数据库迁移、后端测试、全链 E2E |
| 后端规则、状态机、权限 | JUnit；涉及真实 SQL/锁/约束时用 PostgreSQL/Testcontainers | 页面未变化时的 Vitest/Playwright |
| 前端参数、响应适配、字段 Schema | 复用或扩展一个 Vitest 契约用例 | 无后端改动时的 JUnit |
| 财务、课酬、奖励、退款、业绩 | 规则/数据库集成测试、固定历史期间回放和逐笔对账 | 每个需求各建一条 E2E |
| 跨模块关键业务链 | 五条共享 Playwright 主链 | 23 个问题域各建独立 E2E |
| 性能问题 | 只对明确 N/C/T 和阈值的问题做定向压测 | 所有页面统一压测 |

### 2.4 现有平台能力复用门禁

- 列表、简单详情、导入和导出优先复用 `GenericAdminController`；通用导出只扩展一次 filters/columns/queryToken，不为每个业务域复制 export/status/download 路由。
- 员工、课程、班级、客户、优惠等选择和批量回显优先注册到 `AdminLookupController`，不重复建设领域 lookup/resolve。
- 异步进度、重试、事件、死信、审批、审计、幂等和文件签名优先复用 Runtime/SystemOps 现有能力。
- 新接口必须在进度台账记录 `reuseRejectedReason`，说明现有 operation 为什么无法承载；没有证据不得新增。
- `CanonicalIdentity`、`BusinessEvent`、`ExportJob`、`UatEvidence` 等名称是逻辑合同，不要求再建同名通用表；优先映射到现有领域表和平台表。

### 2.5 跨需求共享 operation 的合并规则

下列 31 个 operation 被多个问题共同使用。实现时每个 operation 只能有一个 Controller 方法和一个结构化 DTO；下表各需求的请求/响应片段取并集，不能按需求建立同路径的重复接口，也不能让后实现的需求覆盖先实现字段。

| Operation | 关联问题 | 合并请求合同 | 合并响应合同 |
| --- | --- | --- | --- |
| `GET /api/v1/bi-dashboard-admin/reports/{id}/query-schema` | RPT-03、RPT-06、LOAD-03 | path: id | dimensions[], requiredDimensions[], invalidCombinations[], loadPolicy + loadPolicy, requiredFilters[], maxSyncDays, cacheTtlSeconds + loadPolicy,defaultPeriod,maxSyncDays,timezone,requiredFilters[] |
| `POST /api/v1/bi-dashboard-admin/reports/{id}/queries` | RPT-03、PERM-02、LOAD-03、LOAD-05、QUERY-02、LIST-03 | metricIds[], dimensions, filters, period, groupBy[], idempotencyKey + filters,businessLineIds[],period + period,filters,groupBy[],idempotencyKey + submittedQuery{period,filters,groupBy[]},idempotencyKey + period{fromInclusive,toExclusive,granularity,timezone} + period,filters,groupBy[],includeTotals=true | queryId, status, acceptedFilters + queryId,effectiveScope,scopeHash + queryId,appliedPeriod,timezone,status,result + queryId,echoedQuery,result + queryId,appliedPeriod,result + queryId,records,groupTotals,grandTotal,reconciliationDiff |
| `GET /api/v1/bi-dashboard-admin/reports/{id}/definition` | RPT-05、LIST-03 | path: id, version? + version? | purpose, formula, refreshPolicy, sources[], exclusions[], owner, version, effectiveFrom + detailColumns,groupTotals,totalMetrics,aggregationSemantics |
| `POST /api/v1/bi-dashboard-admin/reports/{id}/query-jobs` | RPT-06、LOAD-04 | filters, period, groupBy[], idempotencyKey + period,filters,groupBy[],idempotencyKey | jobId, status, cacheHit + jobId,status,cacheHit,estimatedSeconds |
| `GET /api/v1/bi-dashboard-admin/report-query-jobs/{id}` | RPT-06、LOAD-04 | path: id | status, progress, result, error, expiresAt + status,progress,result,error,expiresAt |
| `GET /api/v1/crm-admin/customers` | CRM-01、CRM-05 | keyword, assignmentCountMin, lastAssignedById, lastAssignedFrom, lastAssignedTo, page, pageSize + firstContractFrom, firstContractTo, firstCourseIds[], page, pageSize | records 含 assignmentCount,lastAssignedById,lastAssignedByName,lastAssignedAt + records 含 firstContractId,firstContractNo,firstContractAt,firstCourseNames,firstPaidAmount |
| `POST /api/v1/admin/{moduleBase}/{resourceCode}/export` | CRM-01、ASSET-01、TRANS-02、TRANS-11、REFUND-03、ACTIVE-02、QUERY-03、ID-01、ID-03、ID-07、ID-08、OPT-02、OPT-05、OPT-07、BUG-01 | moduleBase/resourceCode + 资源注册的 filters/columns/queryToken；权限与 scopeHash 服务端解析 | 统一导出 taskId/status/resultFileId，任务进度和下载复用 GenericAdmin |
| `GET /api/v1/crm-admin/students` | STU-01、STU-02、STU-04 | operationStatuses[], asOfDate, page, pageSize + businessLineIds[],periodFrom,periodTo,page,pageSize + lastContractFrom,lastContractTo,lastContractBasis,refundDisposition,page,pageSize | records,total,summary + records 含 businessLines[], total + records 含 lastContractId,lastSignedAt,lastPaidAt |
| `GET /api/v1/platform-foundation/roles/{id}/data-scope` | PERM-01、PERM-03 | path: id | orgIds,campusIds,businessLineIds,schoolStages + schoolStages[] 与其他范围 |
| `PUT /api/v1/platform-foundation/roles/{id}/data-scope` | PERM-01、PERM-03 | expectedVersion,orgIds[],campusIds[],businessLineIds[],schoolStages[] + expectedVersion,schoolStages[] 与其他范围 | scopeVersion,affectedUserCount + scopeVersion |
| `GET /api/v1/hr-admin/teacher-level-assignments` | PERM-04、PAY-03 | teacherId,businessLineId,schoolStage,asOf,page,pageSize + teacherId,businessLineId,schoolStage,asOf | records,total |
| `POST /api/v1/hr-admin/teacher-level-assignments` | PERM-04、PAY-03 | teacherId,businessLineId,schoolStage,levelCode,effectiveFrom,effectiveTo,idempotencyKey + teacherId,businessLineId,schoolStage,levelCode,effectiveFrom,effectiveTo | assignmentId,version |
| `POST /api/v1/admin/lookups/{entityType}/resolve` | PERM-04、DISC-07、CON-02、COURSE-01、PARAM-01、APPROVAL-03、PAY-03、PAY-05、PAY-06、QUERY-02 | entityType 按资源注册；teacherId,businessLineId,schoolStage,occurredAt + entityType 按资源注册；ids[] + entityType 按资源注册；names[] + entityType 按资源注册；employeeId,occurredAt + entityType 按资源注册；campusId,regionId,conditions,occurredAt + entityType 按资源注册；preset,anchorDate,campusId | assignmentId,levelCode,ruleEvidence + items[{id,code,name,status,disabled}] + items[] + resolved[],ambiguous[],unmatched[] + items[{id,label,status,disabled}] + employmentType,historyId,evidence + assignmentId,levelCode + resolvedRuleVersion,inheritedFrom,overrideChain[] + employmentType,historyId + periodFromInclusive,periodToExclusive,label,comparisonPeriod |
| `GET /api/v1/academic-admin/course-business-categories` | TARGET-02、COURSE-01 | status,keyword,page,pageSize + keyword,status,page,pageSize | records,total |
| `PUT /api/v1/academic-admin/course-business-categories/{id}` | TARGET-02、PAY-07 | expectedVersion,includeInConsumptionRevenue,effectiveFrom + expectedVersion,compensationMeasureType | categoryVersion |
| `POST /api/v1/contract-admin/quote/calculate` | MALL-01、DISC-02、DISC-04、CON-03、COURSE-02、BUG-02 | customerId,campusId,courseItems[{courseId,pricePlanId,quantity}],promotionContext,discountRuleIds[] + courseItems[],discountRuleIds[],season + pricePlanId,discountRuleIds[] + courseItems[] + courseItems[],discountRuleIds[],promotionContext | quoteId,courseItems[],matchedRules[],totalOriginal,totalDiscount,totalPayable + courseItems[].discountAllocations,totalDiscount,explanations + originalAmount,discountAmount,finalAmount,rules[] + courseItems[].hours,price,roundingAdjustment + pricingPolicyVersion,roundingDetails + matchedRules[],rejectedRules[],allocations[],totalPayable |
| `PUT /api/v1/contract-admin/orders/{id}/performance-allocations` | MALL-03、PERF-04 | expectedVersion,items[{contractItemId,fundingSources[],allocations[]}] + expectedVersion,items[{contractItemId,primaryEmployeeId,collaborators[{employeeId,ratio}],amounts[]}] | allocationVersion,totalRecognizedAmount,difference + allocationVersion,differences[] |
| `GET /api/v1/contract-admin/discount-rules` | DISC-03、DISC-07、CON-02 | keyword,campusId,courseIds[],schoolStage,occurredAt,eligibleOnly,page,pageSize + keyword,ruleTypes[],statuses[],campusId,courseIds[],page,pageSize + keyword,ruleTypes[],campusId,courseIds[],effectiveAt,statuses[],page,pageSize | records,total,disabledReasons + records,total |
| `POST /api/v1/contract-admin/staff-order-records` | DISC-04、CON-01 | 现有 StaffOrderRequest，包含 discountRuleIds[] + customerId,courseItems[],discountRuleIds[],quoteSnapshotId,quoteHash,idempotencyKey | orderId,quoteSnapshotId + orderId,contractItems[],discountRules[] |
| `POST /api/v1/contract-admin/signing-types/evaluate` | CON-06、TRANS-04、TRANS-15 | studentId,courseItems[],signedAt + studentId,courseItems[],signedAt,attributionId? | signingType,ruleVersion,evidenceEvents[],confidence + signingType,ruleVersion,evidenceEvents[] |
| `POST /api/v1/contract-admin/orders/{id}/signing-type-change-requests` | CON-06、TRANS-15 | requestedType,reason,evidence,idempotencyKey + requestedType,reason,evidence | requestId,approvalId |
| `GET /api/v1/admin/{moduleBase}/{resourceCode}/import-export-tasks/{taskId}` | ASSET-01、TRANS-11、OPT-07 | path: moduleBase/resourceCode/taskId | 通用导出任务状态、进度、行数、结果文件和错误信息 |
| `POST /api/v1/hr-admin/class-fee-rules/simulate` | PAY-01、PAY-06 | teacherId,lessonId,occurredAt | matchedRuleVersion,teacherLevel,deductedStudentCount,unitPrice,amount,explanations[] + employmentType,employmentHistoryId,matchedRuleVersion,amount |
| `POST /api/v1/transition-reward-admin/rules` | TRANS-01、TRANS-05 | name,conditions,baseType,action,effectiveFrom,effectiveTo + baseType,fundingSourcePolicies,refundPolicy,discountPolicy | ruleId,version |
| `POST /api/v1/transition-reward-admin/rules/simulate` | TRANS-01、TRANS-10 | ruleVersionId,studentId,contractItemId,occurredAt | matched,base,amount,explanations[] + matched,amount,explanations[] |
| `POST /api/v1/refund-admin/batches/preview` | REFUND-01、REFUND-04 | studentId,items[{contractId,contractItemId,entitlementId,requestedAmount}],asOf,idempotencyKey + items[] 增加 repricePolicyVersion | previewToken,items[{refundableAmount,deductions,warnings}],totals + items[] 增加 discountClawbackAmount 与 repriceSnapshotId |
| `POST /api/v1/admin/resources/{resourceKey}/queries` | LOAD-02、QUERY-01、LIST-01、LIST-02 | filters,page,pageSize,sort,idempotencyKey + filters,page,pageSize,sort + filters[{field,operator=IN,values[]}],page,pageSize + filters,page,pageSize,sort,summaryFields[] | queryToken,records,total,summary,appliedFilters + records,total,summary,queryMeta{elapsedMs,planHash} + records,total,summary,normalizedFilters + records,total,summary,summaryVersion |
| `GET /api/v1/academic-admin/consumption-records` | ID-01、OPT-02 | teacherEmployeeIds[],operatorEmployeeIds[],page,pageSize + filters,page,pageSize | records 含 teacherEmployeeId,teacherEmployeeCode,teacherName,operatorEmployeeId + records 含完整 snapshot 与 snapshotVersion |
| `GET /api/v1/academic-admin/class-students` | ID-04、ID-05、ID-06、ID-07、OPT-03 | courseIds[],page,pageSize + classIds[],page,pageSize + contractIds[],contractItemIds[],entitlementIds[],page,pageSize + entitlementIds[],enrolledCourseIds[],page,pageSize + classStatuses[],asOf,page,pageSize | records 含 courseId,courseCode,courseName + records 含 classId,classCode,className + records 含 contractId,contractNo,contractItemId,entitlementId + records 含 entitlementId,enrolledCourseId,courseId,contractItemId + records 含 classStatus,classStatusChangedAt,total,summary |
| `GET /api/v1/academic-admin/lessons` | ID-04、ID-05 | courseIds[],page,pageSize + classIds[],page,pageSize | records 含 courseId,courseCode,courseName + records 含 classId,classCode,className |
| `GET /api/v1/academic-admin/attendance-consumptions` | ID-04、ID-05、ID-09 | courseIds[],page,pageSize + classIds[],page,pageSize + teachingMode=ONE_TO_MANY,groupIds[],page,pageSize | records 含 courseId,courseCode,courseName + records 含 classId,classCode,className + records 含 groupId,groupCode,groupName |

## 3. 23 个问题域的实施任务（共 132 项、368 条接口引用、297 个唯一 operation，其中 275 个为缺口候选）

### Task 1: Stage 1 · 数据加载策略（LOAD-01、LOAD-02、LOAD-03、LOAD-04、LOAD-05）

**依赖：** 仅依赖当前主分支基线。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V90__xuebang_query_load_policy_and_saved_queries.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 1.1：逐项差异确认与复用决策

1. 对本域 5 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 1.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V90__xuebang_query_load_policy_and_saved_queries.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 1.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 1.4：逐项接口级方案

##### LOAD-01 · 录入客户、排课、考勤、收款等日期型列表默认只加载近一周或当月至今

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 通用 `ResourceView` 进入路由即执行加载，没有按资源配置默认短周期。
- **闭环目标：** 元数据增加 `loadPolicy=SHORT_PERIOD` 和默认周期；服务端仍强制分页，并在界面明确显示当前默认时间。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、管理元数据。
- **页面：** `crm.page-03`（客户档案）、`academic.page-05`（排课中心）、`academic.page-11`（考勤与课消）、`finance.page-05`（收银与资金流水）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V90__xuebang_query_load_policy_and_saved_queries.sql。领域数据方案：新增 `admin_resource_load_policy`，字段含 resourceKey、mode、defaultPeriodPreset、maxSyncDays、requiredFilters 和 version。
- **后端：** 为日期型资源配置 SHORT_PERIOD 加载策略，客户录入、排课、考勤默认近 7 天，收款默认当月至今，所有请求仍强制分页。
- **前端：** 通用资源页从元数据初始化默认日期，醒目展示“当前默认范围”，用户清空后按资源规则恢复或进入空态。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/resources/{resourceKey}/load-policy` | `view` | path: resourceKey | mode,defaultPeriodPreset,maxSyncDays,requiredFilters[],version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/admin/metadata/resources/{resourceKey}/load-policy` | `edit` | expectedVersion,mode,defaultPeriodPreset,maxSyncDays,requiredFilters[] | version,affectedPages[] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/resources/{resourceKey}` | `view` | filters 增加 periodFrom,periodTo,page,pageSize | records,total,summary,appliedLoadPolicy |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：四类资源首屏只请求配置周期且有分页；跨最大同步天数返回 422 并引导异步查询。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LOAD-02 · 学员、已报课程、合同、班级、1v1 学员等大列表打开时不加载，查询后再加载

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 通用资源页及多个专用管理页会在 `onMounted` 请求首屏，没有大列表查询门禁。
- **闭环目标：** 元数据增加 `loadPolicy=MANUAL`；必填至少一个有效条件后才能查询，清空条件恢复空态。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、管理元数据。
- **页面：** `crm.page-03`（客户档案）、`contract.page-04`（合同订单台账）、`academic.page-04`（班级与学员）、`academic.page-01`（教务工作台）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V90__xuebang_query_load_policy_and_saved_queries.sql。领域数据方案：load policy 保存 conditionGroups，后端校验实际过滤条件而非相信前端 hasFilter 标记。
- **后端：** 学员、已报课程、合同、班级和 1v1 学员配置 MANUAL，进入页面只取 Schema，至少一个有效条件满足后才允许查询。
- **前端：** 大列表初始显示查询空态和必填条件提示；清空条件立即清表且不发事实请求。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/pages/{pageCode}` | `view` | path: pageCode | schema,loadPolicy,requiredConditionGroups[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/queries` | `create` | filters,page,pageSize,sort,idempotencyKey | queryToken,records,total,summary,appliedFilters |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/queries/validate` | `view` | filters | valid,satisfiedGroups[],errors[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：Playwright 监听首屏无事实接口；空值/空数组不算有效条件，满足任一条件组后只发一次查询，清空后恢复空态。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LOAD-03 · 课消、考勤、客户分析等时间型报表默认按近一周加载

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** BI 页面没有按报表类型配置近一周默认策略，日期初始化与业务报表类型未绑定。
- **闭环目标：** 每张报表配置默认周期和最大同步跨度；时间型报表默认近 7 天并允许快捷切换。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、BI 元数据。
- **页面：** `bi-dashboard.page-03`（招生 CRM 分析）、`bi-dashboard.page-05`（合同收款与课消）、`bi-dashboard.page-06`（教师教学质量）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V90__xuebang_query_load_policy_and_saved_queries.sql。领域数据方案：扩展报表定义的 loadPolicy、defaultPeriodPreset、maxSyncDays、timezoneSource，并保存到版本。
- **后端：** 课消、考勤、客户分析等时间型报表配置默认近 7 天和最大同步跨度，日期按校区时区计算。
- **前端：** 报表页使用统一业务周期组件初始化近 7 天，显示精确起止时间和时区。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:018-customer-conversion-reports`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/query-schema` | `view` | path: id | loadPolicy,defaultPeriod,maxSyncDays,timezone,requiredFilters[] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | period,filters,groupBy[],idempotencyKey | queryId,appliedPeriod,timezone,status,result |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/bi-dashboard-admin/reports/{id}/load-policy` | `edit` | expectedVersion,mode,defaultPeriodPreset,maxSyncDays,timezoneSource | definitionVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：上海时区周跨月、月末和夏令时无关边界正确；三类报表首查均为连续 7 个自然日。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LOAD-04 · 业绩、转化率、带生量、续班率、退费率等高聚合报表默认不加载

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** `BiDashboard` 初始化会加载定义、实例、报表和数据，未对高聚合报表实施选择条件后手动触发。
- **闭环目标：** 高聚合报表进入时只加载 Schema，不取事实数据；校验周期、组织和必要维度后再提交查询任务。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、BI 查询任务。
- **页面：** `bi-dashboard.page-04`（销售与顾问绩效）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V90__xuebang_query_load_policy_and_saved_queries.sql。领域数据方案：查询任务保存 reportId、filterHash、scopeHash、metricVersions、status、progress、resultRef、expiresAt。
- **后端：** 业绩、转化率、带生量、续班率和退费率配置 MANUAL_ASYNC，进入只加载 Schema，周期、组织和必要维度校验通过后提交任务。
- **前端：** 高聚合报表以查询按钮创建任务，提供进度、取消、失败重试和缓存命中提示。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:004-sales-funnel`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/query-jobs` | `view` | period,filters,groupBy[],idempotencyKey | jobId,status,cacheHit,estimatedSeconds |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/report-query-jobs/{id}` | `view` | path: id | status,progress,result,error,expiresAt |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/report-query-jobs/{id}/cancel` | `view` | expectedVersion | status,cancelledAt |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：五类报表首屏零事实请求；缺周期/组织返回 422；重复提交命中同权限缓存，取消后不再发布结果。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LOAD-05 · 报表应先选按校区/科目等汇总方式，再点查询；切换汇总方式不要自动刷新

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 当前 BI 加载流程没有统一的“编辑条件/已提交条件”双状态，也没有防止维度变化自动请求的机制。
- **闭环目标：** 前端分离 `draftQuery` 与 `submittedQuery`，仅“查询”按钮提交；显示未应用更改提示并缓存最近查询。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、前端状态层、后端 API、BI。
- **页面：** `bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V90__xuebang_query_load_policy_and_saved_queries.sql。领域数据方案：新增 `bi_saved_query` 保存用户最近一次已提交条件和 schemaVersion，服务端仍以请求体为唯一执行输入。
- **后端：** 报表查询表单分离 draftQuery 与 submittedQuery；校区、科目、汇总方式变化只标记未应用，只有点击查询才提交。
- **前端：** 查询区显示未应用更改提示、恢复上次查询和重置；图表标题标识当前已提交汇总方式。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/generated/adminPageManifest.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:039-reports`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/saved-query` | `view` | path: id | submittedQuery,schemaVersion,savedAt |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/bi-dashboard-admin/reports/{id}/saved-query` | `edit` | expectedVersion,submittedQuery,schemaVersion | version,savedAt |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | submittedQuery{period,filters,groupBy[]},idempotencyKey | queryId,echoedQuery,result |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：连续切换校区/科目/汇总方式不产生网络请求；点击查询仅提交最后草稿，刷新后可恢复上次已提交条件。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 1.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-load-policy): close LOAD-01,LOAD-02,LOAD-03,LOAD-04,LOAD-05`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 2: Stage 1 · 数据查询提效（QUERY-01、QUERY-02、QUERY-03）

**依赖：** LOAD。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V91__xuebang_query_performance_snapshots.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/BusinessPeriodPicker.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 2.1：逐项差异确认与复用决策

1. 对本域 3 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 2.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V91__xuebang_query_performance_snapshots.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 2.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 2.4：逐项接口级方案

##### QUERY-01 · 客户、学员、排课、考勤等常见列表查询响应慢

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 新平台普遍采用后端分页并为部分关系字段建索引，但本次没有大数据量基准测试，不能证明常用组合查询达到目标。
- **闭环目标：** 用脱敏生产量级数据建立 P50/P95 基线；按慢 SQL 补组合索引、覆盖索引和查询投影，验收 P95。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL、性能测试。
- **页面：** `crm.page-03`（客户档案）、`academic.page-05`（排课中心）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V91__xuebang_query_performance_snapshots.sql。领域数据方案：新增查询基准数据工厂和 `ops_query_performance_baseline`；V107 按 EXPLAIN 证据创建索引，禁止无依据全表索引。
- **后端：** 以脱敏生产量级建立客户、学员、排课和考勤组合查询 P50/P95 基线，按慢 SQL 增加组合/覆盖索引和只读投影。
- **前端：** 页面只增加稳定排序、查询耗时与超时提示，不用前端分页掩盖后端性能问题。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/ops/SystemOpsController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/ops/SystemOpsModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/ops/SystemOpsService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/components/business/BusinessPeriodPicker.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/systemOpsAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/system-ops-admin/query-performance/runs` | `view` | scenarioIds[],datasetScale,iterations,idempotencyKey | runId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/system-ops-admin/query-performance/runs/{id}` | `view` | path: id | scenarios[{p50Ms,p95Ms,rows,planHash,targetMet}],summary |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/queries` | `create` | filters,page,pageSize,sort | records,total,summary,queryMeta{elapsedMs,planHash} |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：百万级脱敏夹具执行固定场景，常用列表 P95 达项目门槛；计划哈希稳定且分页无重复/漏行。
- **前置依赖：** LOAD。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### QUERY-02 · 报表日期选择增加周、月、季、年快捷项

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 页面存在日期范围组件，但未发现统一的周/月/季/年快捷选择及自然周期边界规则。
- **闭环目标：** 提供统一 `BusinessPeriodPicker`，支持自然周/月/季/年、上期同期和校区时区，输出明确起止时间。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、前端共享组件、后端 API。
- **页面：** `bi-dashboard.page-03`（招生 CRM 分析）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V91__xuebang_query_performance_snapshots.sql。领域数据方案：查询 Schema 增加 periodGranularities、timezone、weekStartsOn、fiscalYearStartMonth，API 统一接收 periodFromInclusive/periodToExclusive。
- **后端：** 实现统一 BusinessPeriodPicker，支持自然周/月/季/年、上期、去年同期和自定义范围，按校区时区输出闭开区间。
- **前端：** 替换 BI 和时间型业务页面散落的日期范围组件，快捷项展示实际起止日期。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`；前端 `SourceCode/dinuo-admin-vben/src/components/business/BusinessPeriodPicker.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:018-customer-conversion-reports`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/business-period-schema` | `view` | campusId,resourceKey | timezone,weekStartsOn,fiscalYearStartMonth,allowedGranularities[] |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；preset,anchorDate,campusId | periodFromInclusive,periodToExclusive,label,comparisonPeriod |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | period{fromInclusive,toExclusive,granularity,timezone} | queryId,appliedPeriod,result |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：周跨月、季度跨年、闰年、上期和去年同期边界快照固定；所有报表提交同一闭开区间合同。
- **前置依赖：** LOAD。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### QUERY-03 · 三个月以上报表查询和导出需提速

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 已有 BI 快照/缓存思路和异步导出底座，但各业务报表未接入统一长周期任务，也无性能指标。
- **闭环目标：** 长周期查询走日/月快照、分区聚合和异步任务；页面展示进度，导出复用同一结果集，避免二次全表计算。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI、导出中心。
- **页面：** `bi-dashboard.page-11`（自助报表）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V91__xuebang_query_performance_snapshots.sql。领域数据方案：新增 `bi_aggregate_snapshot`、`bi_query_result_set` 和月分区水位；结果集记录范围哈希、口径版本和过期时间。
- **后端：** 三个月以上查询切换到日/月快照、分区聚合和异步任务，导出复用同一 resultSetId，避免重复扫描事实表。
- **前端：** 长周期查询显示预计耗时、任务进度和结果有效期；导出按钮直接基于已完成结果集创建文件。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/components/business/BusinessPeriodPicker.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:039-reports`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/long-period-query-jobs` | `view` | reportId,period,filters,groupBy[],idempotencyKey | jobId,status,aggregationLevel |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/long-period-query-jobs/{id}` | `view` | path: id | status,progress,resultSetId,rowCount,expiresAt |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：columns[],format,idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：90/91/365 天边界选择正确执行路径；导出不新增事实 SQL，查询与导出行数、合计及口径版本一致。
- **前置依赖：** LOAD。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 2.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-query-performance): close QUERY-01,QUERY-02,QUERY-03`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 3: Stage 1 · 列表数据与汇总（LIST-01、LIST-02、LIST-03）

**依赖：** PERM。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V92__xuebang_list_filter_summary_contract.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 3.1：逐项差异确认与复用决策

1. 对本域 3 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 3.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V92__xuebang_list_filter_summary_contract.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 3.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 3.4：逐项接口级方案

##### LIST-01 · 校区、档期、学员状态等重要查询字段支持多选

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 元数据筛选器可以承载多选，但多数专用 API/页面仍使用单值参数，未形成关键字段多选规范。
- **闭环目标：** 列表筛选统一接收数组；空数组、全选和未传参数定义一致，服务端使用参数化 `IN` 并限制数量。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、共享查询合同。
- **页面：** `crm.page-03`（客户档案）、`contract.page-04`（合同订单台账）、`academic.page-05`（排课中心）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V92__xuebang_list_filter_summary_contract.sql。领域数据方案：扩展 AdminRequest FilterValue 为 scalar/array/range，查询构建器复用安全数组绑定且拒绝未知枚举。
- **后端：** 重要维度统一用数组参数，空数组、未传、显式全选均定义为不限制；参数化 IN 限制最大 200 个值并叠加权限范围。
- **前端：** 校区、档期、学员状态、业务线和课程使用分页远程多选，选中项以 ID 保存。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/queries` | `create` | filters[{field,operator=IN,values[]}],page,pageSize | records,total,summary,normalizedFilters |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/metadata/resources/{resourceKey}/filters/validate` | `view` | filters[] | valid,normalizedFilters[],errors[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/lookups/{lookupType}` | `view` | keyword,selectedIds[],page,pageSize | records,total |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未传、空数组和全枚举结果一致，部分选择准确；201 个值返回 422，伪造越权校区返回 403。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LIST-02 · 列表按当前查询条件展示总数、合计金额等总汇总

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 分页接口通常返回总条数，但没有为合同、收款、课消等统一返回当前筛选下的金额/课时等合计。
- **闭环目标：** 列表响应增加独立 `summary`，与明细复用同一过滤器和权限条件；合计不得只计算当前页。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、共享列表组件。
- **页面：** `contract.page-04`（合同订单台账）、`finance.page-05`（收银与资金流水）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V92__xuebang_list_filter_summary_contract.sql。领域数据方案：PageResult 增加 summary 与 summaryVersion；查询服务使用共享 CTE/谓词生成明细 SQL 和汇总 SQL，禁止按当前页求和。
- **后端：** 分页响应增加全量筛选 summary，合同、收款、课消分别声明金额、课时、人头等聚合字段，明细和汇总复用同一权限过滤器。
- **前端：** 数据表底部固定显示总条数和业务合计，明确“按当前全部查询条件”。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:001-quotation-orders`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/queries` | `create` | filters,page,pageSize,sort,summaryFields[] | records,total,summary,summaryVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/resources/{resourceKey}/summary-schema` | `view` | path: resourceKey | fields[{key,label,valueType,unit,formula}] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/resources/{resourceKey}/summaries` | `create` | filters,summaryFields[] | summary,filterHash,scopeHash |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：多页数据中当前页合计与总汇总不同且总汇总正确；列表、独立汇总和导出统计使用相同 filterHash/scopeHash。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### LIST-03 · 报表按当前查询条件显示总汇总，无需导出手算

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** BI 有指标卡和图表，但未保证每张明细报表都返回与当前过滤条件一致的总计行。
- **闭环目标：** 报表定义声明总汇总指标；后端同一次查询返回明细和 grand total，并提供分组小计/总计一致性测试。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、BI。
- **页面：** `bi-dashboard.page-05`（合同收款与课消）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V92__xuebang_list_filter_summary_contract.sql。领域数据方案：扩展 `bi_report_definition` 的 totalMetrics/aggregationSemantics，结果集保存 total reconciliation。
- **后端：** 每张明细报表声明 grand total 指标和分组小计，后端一次查询返回明细、groups 和 grandTotal，并校验可加/不可加指标。
- **前端：** 报表表格固定展示分组小计和总计，不再要求导出手算；不可加比率展示分子/分母再计算。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/adminRequest.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:005-analytics-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/definition` | `view` | version? | detailColumns,groupTotals,totalMetrics,aggregationSemantics |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | period,filters,groupBy[],includeTotals=true | queryId,records,groupTotals,grandTotal,reconciliationDiff |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/query-results/{id}/totals` | `view` | path: id | groupTotals,grandTotal,metricEvidence[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：金额可加指标的小计之和等于总计；转化率按总分子/总分母重算而非平均，页面与导出总计一致。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 3.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-list-summary): close LIST-01,LIST-02,LIST-03`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 4: Stage 1 · 重要数据 ID（ID-01、ID-02、ID-03、ID-04、ID-05、ID-06、ID-07、ID-08、ID-09）

**依赖：** 仅依赖当前主分支基线。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V93__xuebang_stable_id_projection_and_legacy_mapping.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 4.1：逐项差异确认与复用决策

1. 对本域 9 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 4.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V93__xuebang_stable_id_projection_and_legacy_mapping.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 4.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 4.4：逐项接口级方案

##### ID-01 · 考勤列表、扣费记录和导出需显示员工/教师 ID

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 领域表内部保存教师/员工 ID，但专用考勤、扣费导出未闭环；HR 员工导出也未输出员工 ID。
- **闭环目标：** 所有明细 API 和导出固定输出员工 UUID、员工编号和姓名，姓名仅用于展示，关联以 ID 为准。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、考勤/课消/导出。
- **页面：** `academic.page-11`（考勤与课消）、`hr.page-08`（考勤与排班）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：扩展考勤、课消投影字段 teacher_employee_id/operator_employee_id，并在导出 Schema 注册 employeeId/employeeCode。
- **后端：** 考勤和扣费所有明细合同固定输出员工/教师 UUID、员工编号和姓名，导出列保持同名同序，业务关联只使用 ID。
- **前端：** 考勤、扣费表格和导出默认显示教师 ID/编号/姓名，ID 支持复制与精确筛选。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:003-attendance-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/attendance-records` | `view` | teacherEmployeeIds[],operatorEmployeeIds[],page,pageSize | records 含 teacherEmployeeId,teacherEmployeeCode,teacherName,operatorEmployeeId |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/consumption-records` | `view` | teacherEmployeeIds[],operatorEmployeeIds[],page,pageSize | records 含 teacherEmployeeId,teacherEmployeeCode,teacherName,operatorEmployeeId |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：recordType,filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名教师夹具能按 UUID 区分；页面/API/导出三端 ID、编号、姓名逐行一致，缺 ID 的历史数据进入异常清单。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-02 · 客户 ID 当前无问题，应在新平台继续保留

- **补充 PRD 基线：** 明确覆盖；✅ 已解决。
- **最新主分支证据：** CRM 客户使用稳定 UUID，分配、合并、关系和合同链均以 ID 关联，测试覆盖客户关系闭环。
- **闭环目标：** 数据迁移保留原系统 ID 为 `legacyId`，新 UUID 为主键；导出同时提供两者便于过渡对账。
- **实施模式：** `REUSE_EXISTING`。
- **改动端：** 数据迁移、数据治理。
- **页面：** `crm.page-03`（客户档案）、`finance.page-19`（财务异常与迁移）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：默认不改业务表。历史迁移确认需要双 ID 对账时，才在现有客户表或迁移映射中增加 legacySystem/legacyId，并以源数据映射和异常清单验收。
- **后端：** 保持现有客户 UUID 主链不变；只有实际导入学邦历史数据时才补 legacySystem/legacyId 映射与对账，不新增运行时 ID 解析服务。
- **前端：** 默认不改页面；只有迁移期业务确需同时检索或导出新旧 ID 时，才扩展现有客户列表和通用导出列。
- **候选落地文件：** 后端 无默认改动；前端 无默认改动；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

- **接口级决策：** 无默认新增接口；复用现有客户查询、AdminLookup 和通用导出。

- **最小验证：** 复用稳定 UUID 和客户关系现有测试；历史迁移发生时增加一条新旧 ID 一对一映射及异常清单对账，不新增独立 E2E。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-03 · 课消分析只有学员姓名、无学员 ID，无法唯一匹配

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 新平台课消/权益关系内部使用稳定学员 UUID，但当前没有经过验收的课消分析专用导出确保该 ID 对外显示。
- **闭环目标：** 课消查询和导出强制列出学员 UUID、学员编号、姓名，姓名重复时给出家庭/校区辅助信息。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、BI/课消导出。
- **页面：** `bi-dashboard.page-05`（合同收款与课消）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：课消贡献事实补 canonical_student_id/student_code/family_id/campus_id 快照并建立按稳定 ID 的查询索引。
- **后端：** 课消分析和导出强制输出稳定学员 UUID、学员编号和姓名；重名时附家庭 ID 和校区 ID 作为辅助信息。
- **前端：** 课消明细首列显示学员 ID/编号，可精确检索、复制和跳转 Student 360。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:005-analytics-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/consumption-analysis` | `view` | studentIds[],studentCodes[],period,page,pageSize | records 含 studentId,studentCode,studentName,familyId,campusId |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/consumptions/{id}/identity-evidence` | `view` | path: id | studentId,studentCode,familyId,campusId,sourceEntitlementId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两个同名学员的分析和导出能以 UUID 唯一匹配，逐笔课消回到正确权益和家庭，汇总不串档。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-04 · 在班学员、排课、班课考勤、扣费等页面需课程 ID

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 后端班级、课次和权益均关联课程 ID；不同页面和导出尚无统一字段合同。
- **闭环目标：** 建立跨页面字段字典和自动契约测试，以上页面/API/导出统一输出 `courseId/courseCode/courseName`。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、跨页面字段合同。
- **页面：** `academic.page-04`（班级与学员）、`academic.page-05`（排课中心）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：创建业务字段目录 `academic.course.identity`，各查询投影明确 course_id，禁止仅按课程名称关联。
- **后端：** 在班学员、排课、班课考勤和扣费统一输出 courseId/courseCode/courseName，并由共享字段 Schema 驱动页面和导出。
- **前端：** 四类页面复用课程身份列组件和远程 ID 筛选；导出表头来自同一 Schema。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/business-fields/academic.course.identity` | `view` | version? | fields[{key,label,type,required,exportable}] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students` | `view` | courseIds[],page,pageSize | records 含 courseId,courseCode,courseName |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/academic-admin/lessons` | `view` | courseIds[],page,pageSize | records 含 courseId,courseCode,courseName |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/attendance-consumptions` | `view` | courseIds[],page,pageSize | records 含 courseId,courseCode,courseName |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：四类 API、页面列和导出均有相同三字段；重名课程用 ID 区分，契约快照防止字段丢失。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-05 · 在班学员、排课、班课考勤、扣费等页面需班级 ID

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 后端关系中保存班级 ID，但通用/专用页面导出不保证均展示。
- **闭环目标：** 统一输出 `classId/classCode/className`，所有跨表加工使用 ID，名称只作展示。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、跨页面字段合同。
- **页面：** `academic.page-04`（班级与学员）、`academic.page-05`（排课中心）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：创建业务字段目录 `academic.class.identity`，补充查询投影和 class_id/start_at 组合索引。
- **后端：** 在班学员、排课、班课考勤和扣费统一输出 classId/classCode/className，所有跨表加工以 classId 连接。
- **前端：** 四类页面复用班级身份列组件，支持班级 ID 精确检索、复制和导出。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/business-fields/academic.class.identity` | `view` | version? | fields[{key,label,type,required,exportable}] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students` | `view` | classIds[],page,pageSize | records 含 classId,classCode,className |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/academic-admin/lessons` | `view` | classIds[],page,pageSize | records 含 classId,classCode,className |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/attendance-consumptions` | `view` | classIds[],page,pageSize | records 含 classId,classCode,className |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名班级夹具在四类查询和导出均按 UUID 区分，页面跳转使用 classId，名称修改不破坏历史关系。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-06 · 在班学员需显示其实际使用的合同 ID

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 在班关系可追到权益，但当前列表没有把权益来源订单/合同投影为明确合同 ID。
- **闭环目标：** 报班/权益建立不可丢失的来源合同项关系；在班学员显示合同 ID、合同项 ID和当前扣费权益 ID。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、合同/权益/班级。
- **页面：** `academic.page-04`（班级与学员）、`contract.page-04`（合同订单台账）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：为 enrollment/class_student 关系增加 source_contract_item_id/source_entitlement_id 非空约束；历史缺失行进入修复表。
- **后端：** 在班关系必须追到来源合同项和当前扣费权益，列表输出 contractId、contractItemId、entitlementId 并可跳转。
- **前端：** 在班学员增加合同 ID、合同项 ID、扣费权益 ID 列和精确筛选。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students` | `view` | contractIds[],contractItemIds[],entitlementIds[],page,pageSize | records 含 contractId,contractNo,contractItemId,entitlementId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students/{id}/source-chain` | `view` | path: id | enrollmentId,contract,contractItem,entitlement,consumptionSummary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/enrollment-source-repairs` | `execute` | enrollmentIds[],mappingDecisions[],idempotencyKey | repairJobId,acceptedCount |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：报班→合同项→权益→扣费链 ID 完整；转班后保留原权益来源，缺失映射不能静默显示错误合同。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-07 · 在班学员需显示已报读课程 ID，便于快速检索

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 现有列表主要围绕班级/课程关系，未发现可直接检索的学员已报课程（权益）ID 列。
- **闭环目标：** 显示 `entitlementId/enrolledCourseId`、课程 ID和合同项 ID，并支持复制、筛选和导出。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、权益/在班学员。
- **页面：** `academic.page-04`（班级与学员）、`academic.page-02`（课程产品）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：统一 enrolledCourseId 为权益业务编号、entitlementId 为 UUID，建立唯一映射和字段目录。
- **后端：** 在班学员输出 entitlementId/enrolledCourseId、courseId 和 contractItemId，支持 ID 复制、筛选、跳转与导出。
- **前端：** 列表新增已报课程 ID 列和远程多选，名称仅辅助展示。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students` | `view` | entitlementIds[],enrolledCourseIds[],page,pageSize | records 含 entitlementId,enrolledCourseId,courseId,contractItemId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/entitlements/resolve` | `view` | entitlementId?\|enrolledCourseId? | entitlementId,enrolledCourseId,studentId,courseId,contractItemId |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员多个同名课程权益可按已报课程 ID 准确检索，页面/API/导出来源链一致。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-08 · 教室管理导出需教室 ID，校区内可能重名

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 教室有内部 ID，但未发现教室专用导出和“ID+校区”输出合同。
- **闭环目标：** 教室导出增加 UUID、教室编码、名称、校区 ID；同校区名称可提示重复，业务关联一律用 ID。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教室/导出。
- **页面：** `academic.page-03`（教室管理）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：教室编码在校区内唯一；对重名只告警不作为关联键，新增导出字段 Schema。
- **后端：** 教室查询和导出固定包含 classroomId、classroomCode、classroomName、campusId/campusName，业务关系一律用 ID。
- **前端：** 教室管理增加 ID/编码列、同校区重名提示和异步导出。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:018-classroom-management`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/classrooms` | `view` | classroomIds[],campusIds[],keyword,page,pageSize | records 含 classroomId,classroomCode,classroomName,campusId,campusName,duplicateNameWarning |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/classrooms/duplicate-name-check` | `create` | campusId,classroomName,excludeId? | duplicates[],blocking=false |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两个校区同名、同校区同名和编码冲突分别符合规则；导出用 UUID+校区唯一定位教室。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ID-09 · 1vN 考勤列表需小组/小组班 ID，不能只有名称

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 1vN 可复用班级关系，但考勤列表/导出没有明确的小组 ID 字段契约。
- **闭环目标：** 将 1vN 小组建模为班级子类型或独立 group，并统一输出 `groupId/groupCode/groupName`。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、1vN 考勤/导出。
- **页面：** `academic.page-04`（班级与学员）、`academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V93__xuebang_stable_id_projection_and_legacy_mapping.sql。领域数据方案：新增/扩展 `academic_group` 与 lesson/attendance/consumption 的 group_id 外键，历史小组名称迁移到稳定 ID。
- **后端：** 将 1vN 小组建模为 GROUP 类型班级或显式 group，并在考勤/扣费统一输出 groupId/groupCode/groupName。
- **前端：** 1vN 考勤增加小组 ID/编码列、筛选和导出，名称变更保留历史快照。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/contracts/entityLookup.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/recordAdapters.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/groups` | `view` | groupIds[],classId?,keyword,page,pageSize | records 含 groupId,groupCode,groupName,classId |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/attendance-consumptions` | `view` | teachingMode=ONE_TO_MANY,groupIds[],page,pageSize | records 含 groupId,groupCode,groupName |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/groups/attendance-exports` | `export` | filters,columns[],idempotencyKey | exportTaskId,schemaVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名小组按 UUID 区分，1vN 考勤页面/API/导出均有三字段；改名后历史记录仍指向同一小组。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 4.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-important-ids): close ID-01,ID-02,ID-03,ID-04,ID-05,ID-06,ID-07,ID-08,ID-09`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 5: Stage 2 · 课程设置（COURSE-01、COURSE-02）

**依赖：** ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V94__xuebang_course_classification_and_pricing_precision.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 5.1：逐项差异确认与复用决策

1. 对本域 2 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 5.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V94__xuebang_course_classification_and_pricing_precision.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 5.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 5.4：逐项接口级方案

##### COURSE-01 · 班课、1v1、1vN分别设置导致同类型不同名称和查询多选

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 新平台统一使用 `course_type/class_type` 枚举 `GROUP/ONE_ON_ONE/ONE_TO_N/TRIAL`，但课程名称只约束课程编码唯一，未建立经营分类/别名治理。
- **闭环目标：** 课程类型只用标准字典；“引流/活动/常规”等作为经营分类；历史名称映射到同一分类 ID。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `academic.page-02`（课程产品）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V94__xuebang_course_classification_and_pricing_precision.sql。领域数据方案：新增 `academic_course_type`、`academic_course_business_category`、`academic_course_category_alias` 和课程映射版本。
- **后端：** 课程类型只使用标准字典，经营分类独立维护；历史名称通过别名映射到标准分类 ID。
- **前端：** 课程产品页将类型/经营分类拆分为远程选择器，提供别名冲突合并。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:032-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/course-types` | `view` | keyword,status,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/course-business-categories` | `view` | keyword,status,page,pageSize | records,total |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；names[] | resolved[],ambiguous[],unmatched[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：班课/1v1/1vN 标准类型不因显示名产生多条枚举，历史别名迁移可复核。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### COURSE-02 · 课程总价与课时精度造成 5959.2/5960、120/120.02

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 价格与课时均保留两位小数，没有定价模式和舍入差额规则。
- **闭环目标：** 按期定价锁定总价和整数课时；按课时定价明确精度、舍入方向和尾差科目，报价和课消共用快照。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工制单端、后端 API、财务。
- **页面：** `academic.page-02`（课程产品）、`contract.page-01`（价格与套餐）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V94__xuebang_course_classification_and_pricing_precision.sql。领域数据方案：课程增加 pricingPolicyId；价格方案保存 scale/roundingMode；报价和课消引用同一价格快照。
- **后端：** 统一课程与价格精度策略；按期锁定总价和整数课时，按课时明确精度、舍入方向和尾差科目。
- **前端：** 课程和价格页提供计算示例，报价显示尾差归属并禁止前端自行计算。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:032-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/courses/{id}/pricing-policies` | `create` | mode,hoursScale,priceScale,roundingMode,adjustmentSubjectId,effectiveFrom | pricingPolicyId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/courses/{id}/pricing-preview` | `view` | pricingPolicyId,totalPrice,hours,quantity | unitPrice,normalizedHours,total,roundingAdjustment |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | courseItems[] | pricingPolicyVersion,roundingDetails |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：5959.2/5960、120/120.02 和多次课消累计尾差算例在报价、课消、财务一致。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 5.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-courses): close COURSE-01,COURSE-02`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 6: Stage 2 · 常用参数（PARAM-01）

**依赖：** 仅依赖当前主分支基线。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V95__xuebang_custom_field_option_lifecycle.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 6.1：逐项差异确认与复用决策

1. 对本域 1 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 6.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V95__xuebang_custom_field_option_lifecycle.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 6.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 6.4：逐项接口级方案

##### PARAM-01 · 自定义字段单选/多选值不能停用，只能删除/修改，影响历史

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 现有“自定义字段/自定义属性”主要是通用资源页，字段 Schema 没有选项实体、选项状态和历史快照。
- **闭环目标：** 建立 `CustomFieldOption`，支持启用/停用、排序、生效期；历史记录保存 option ID+label 快照，已停用值只回显不可新选。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL、所有动态表单。
- **页面：** `platform.page-09`（基础配置与帮助）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V95__xuebang_custom_field_option_lifecycle.sql。领域数据方案：新增 `sys_custom_field`、`sys_custom_field_option`、`sys_custom_field_option_version` 和引用统计投影。
- **后端：** 建立 CustomFieldOption 生命周期，支持启用/停用、排序和生效期，历史数据保存 optionId+label 快照。
- **前端：** 基础配置页提供选项停用和引用影响预览；业务表单对停用项只回显不可新选。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:012-theme-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/custom-fields/{fieldId}/options` | `view` | status,asOf,keyword,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/custom-fields/{fieldId}/options` | `create` | code,label,sortNo,effectiveFrom | optionId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/custom-field-options/{id}/disable` | `create` | expectedVersion,effectiveTo,reason | status,referenceCount |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；ids[] | items[{id,label,status,disabled}] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：停用值不能新选、历史记录继续显示原标签，删除被引用选项返回 409。
- **前置依赖：** 当前主分支基线。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 6.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-parameters): close PARAM-01`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 7: Stage 2 · 员工与审批（APPROVAL-01、APPROVAL-02、APPROVAL-03、APPROVAL-04）

**依赖：** PERM。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V96__xuebang_approval_reapply_and_employment_history.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 7.1：逐项差异确认与复用决策

1. 对本域 4 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 7.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V96__xuebang_approval_reapply_and_employment_history.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 7.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 7.4：逐项接口级方案

##### APPROVAL-01 · 审批节点人员变动需重配整个流程

- **补充 PRD 基线：** 明确覆盖；✅ 已解决。
- **最新主分支证据：** 审批模板节点可单独 upsert；审批人支持申请人上级、校区负责人、财务/HR等动态解析，进行中的待办也可转交。
- **闭环目标：** 上线时审批节点优先绑定角色/组织关系，避免写死姓名；离职时自动扫描并转交待办。
- **实施模式：** `REUSE_EXISTING`。
- **改动端：** 管理端 PC、后端 API、审批中心、HR 事件。
- **页面：** `platform.page-05`（审批流程）、`hr.page-19`（人事审批）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：复用 `sys_approval_task`、`sys_approval_comment`、审批通知和运行时事件，不新增 `sys_approval_task_transfer`。
- **后端：** 复用现有审批转交能力、审批评论、通知和事件；仅补员工离职/调岗事件触发的待办影响扫描与自动转交策略。
- **前端：** 流程设计器继续使用角色/组织关系审批人；人事变更页只在影响扫描确有结果时展示待办数量、建议接收人和处理结果。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:004-approval-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/runtime/approvals/{id}/transfer` | `create` | assigneeName,opinion,expectedVersion | approvalInstance，转交评论、通知和事件已落库 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/runtime/approval-tasks/scan-assignee-impact` | `create` | employeeId,effectiveAt,changeType | tasks[],suggestedAssignees[],unresolved[] |

- **最小验证：** 扩展现有审批服务集成测试，覆盖离职员工有待办时的影响扫描、转交和异常池；不新建审批域 Playwright 套件。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### APPROVAL-02 · 驳回重提只能从头；需可选当前节点或从头

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 数据库 `reject_mode` 支持从头/退回节点等枚举，但 `reapply()` 当前始终调用第一个节点，未消费该配置。
- **闭环目标：** 重提 API 增加 `reapplyMode`；保存驳回节点，按模板允许从头、从驳回节点或指定上游节点重提。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工/业务端接口、后端 API、审批中心。
- **页面：** `platform.page-05`（审批流程）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V96__xuebang_approval_reapply_and_employment_history.sql。领域数据方案：审批实例增加 rejectedNodeId/reapplyMode；模板节点增加 allowedReapplyModes。
- **后端：** 驳回时保存节点，重提支持 FROM_START、FROM_REJECTED_NODE、FROM_SELECTED_UPSTREAM 三种模式并受模板约束。
- **前端：** 驳回详情显示可重提路径；发起端必须选择模式并预览将重新执行的节点。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:004-approval-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/runtime/approvals/{id}/reapply-options` | `view` | path: id | modes[],nodes[],rejectedNodeId |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/admin/runtime/approvals/{id}/reapply` | `create` | reapplyMode,fromNodeId?,changes,reason,idempotencyKey | approvalId,currentNodeId,reopenedTasks[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：三种模式分别验证节点执行序列；模板禁用模式返回 400；已完成实例不能重提。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### APPROVAL-03 · 全职/兼职切换需记录生效日期，便于人效统计

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** HR 有岗位/调动记录和薪资档案生效期，但员工变更请求没有全兼职类型及计划生效日，变更在执行时立即生效。
- **闭环目标：** 建立用工身份历史表，变更单含 `effectiveFrom/effectiveTo`，排课、课酬和人效按业务发生日匹配身份。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工端接口、后端 API、HR/课酬。
- **页面：** `hr.page-02`（员工管理）、`hr.page-06`（入转调离与员工关怀）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V96__xuebang_approval_reapply_and_employment_history.sql。领域数据方案：新增 `hr_employment_type_history`，排除同员工重叠区间；员工主表仅保留当前投影。
- **后端：** 建立用工身份历史，变更单明确 effectiveFrom/effectiveTo，排课、课酬和人效按业务发生日解析身份。
- **前端：** 员工详情增加全职/兼职历史时间线和未来生效变更。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:002-teacher-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/employees/{id}/employment-types` | `view` | asOf?,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/employees/{id}/employment-type-changes` | `create` | employmentType,effectiveFrom,effectiveTo?,reason,idempotencyKey | changeId,approvalId |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；employeeId,occurredAt | employmentType,historyId,evidence |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：全职转兼职前后课次分别匹配正确身份，重叠生效期被阻断，历史工资不被当前身份改写。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### APPROVAL-04 · 结转审批提交前校验目标班容量，避免审批时报满员

- **补充 PRD 基线：** 明确覆盖；✅ 已解决。
- **最新主分支证据：** 售后转课 `createChange()` 提交前检查 `max_students/enrolled_count`，执行时用条件更新再次防止并发超员。测试已覆盖转课执行。
- **闭环目标：** 保留双重校验；审批详情显示申请时容量、当前容量和容量变化告警。
- **实施模式：** `REUSE_EXISTING`。
- **改动端：** 管理端 PC、员工端接口、后端 API、教务、审批中心。
- **页面：** `contract.page-08`（合同变更与退转）、`academic.page-04`（班级与学员）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：复用 `contract_change_application` 的 beforeSummary/afterSummary、版本和审计字段；不新建容量快照表。
- **后端：** 复用现有售后变更提交校验和执行时的条件更新防超员；仅当审批详情缺少提示时，在现有变更详情响应中补申请时容量、当前容量和变化告警。
- **前端：** 仅在现有审批详情中补容量变化提示和满员禁用原因，不另建结转执行页面或换一套 API。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/aftersales/AfterSalesAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/aftersales/AfterSalesAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/aftersales/AfterSalesAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/afterSalesAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:007-refund-transfer`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/after-sales-admin/changes` | `create` | 现有 ChangeRequest，转课时包含 targetCourseId/targetClassId | 现有变更单，提交前已校验容量 |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/after-sales-admin/changes/{id}/execute` | `execute` | 现有 ChangeExecutionRequest | 现有执行结果，条件更新防止并发超员 |

- **最小验证：** 复用 AfterSalesRelationClosureTest；只有新增容量变化提示时补一个响应断言，并保留现有执行防超员覆盖，不新建独立 E2E。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 7.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-approval): close APPROVAL-01,APPROVAL-02,APPROVAL-03,APPROVAL-04`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 8: Stage 2 · 权限与目标（PERM-01、PERM-02、PERM-03、PERM-04、PERM-05、TARGET-01、TARGET-02、TARGET-03、TARGET-04）

**依赖：** ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V97__xuebang_scope_and_target_governance.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 8.1：逐项差异确认与复用决策

1. 对本域 9 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 8.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V97__xuebang_scope_and_target_governance.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 8.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 8.4：逐项接口级方案

##### PERM-01 · 单校区班课与个性化由不同校长负责，客户/学员按业务隔离；同一学员可能同时属于两业务

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** `DataScopeService` 只解析集团/区域/组织/部门/校区/本人等范围，没有业务线范围。
- **闭环目标：** 数据权限增加 `businessLineIds`，客户、权益、课消、合同和员工岗位均带业务线；同一学员主档共享，业务数据按关系授权。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、权限服务、全业务查询。
- **页面：** `platform.page-03`（账号与角色）、`platform.page-04`（权限中心）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `sys_role_business_line_scope`、业务对象 business_line_id 关系及权限范围哈希。
- **后端：** 将 businessLineIds 纳入用户数据范围，客户主档共享，客户/权益/课消/合同/岗位关系按业务线授权。
- **前端：** 角色授权页增加业务线多选和冲突预览；业务页面显示当前生效范围。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:002-accounts-roles`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/roles/{id}/data-scope` | `view` | path: id | orgIds,campusIds,businessLineIds,schoolStages |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/platform-foundation/roles/{id}/data-scope` | `edit` | expectedVersion,orgIds[],campusIds[],businessLineIds[],schoolStages[] | scopeVersion,affectedUserCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/data-scope/evaluate` | `view` | userId,resourceKey,objectIds[] | decisions[{objectId,allowed,reason}] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员两业务线夹具下，班课校长只能见班课关系，主档基础信息共享但个性化事实被阻断。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERM-02 · 集团层面班课与个性化数据隔离

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 总部角色目前通常获得系统全量或组织/校区范围，不能按班课/个性化隔离。
- **闭环目标：** 增加集团业务线角色和字段/导出规则，BI 查询同样强制业务线数据范围。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、权限服务、BI/导出。
- **页面：** `platform.page-04`（权限中心）、`bi-dashboard.page-10`（指标口径与目标）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：扩展角色范围及 `sec_export_approval`，查询/缓存/导出记录保存 scopeHash。
- **后端：** 增加集团业务线角色和字段/导出策略，所有 BI 查询和导出任务强制注入 businessLineIds。
- **前端：** 权限中心配置集团业务线角色；报表和导出展示已应用业务线范围且不可由前端清空绕过。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:003-field-permissions`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/data-scope/query-constraints` | `view` | resourceKey,requestedFilters | effectiveFilters,scopeHash,deniedFilters[] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | filters,businessLineIds[],period | queryId,effectiveScope,scopeHash |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/admin/runtime/tasks` | `create` | taskType=EXPORT,resourceKey,filters,columns | taskId,effectiveScope,approvalRequired |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：集团班课角色查询/导出只包含班课，伪造个性化业务线返回 403，缓存不能跨 scopeHash 命中。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERM-03 · 班课中学/小学在校区端和集团端按主管隔离

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 没有学段数据范围；课程学科/年级也没有映射为权限维度。
- **闭环目标：** 增加 `schoolStage` 维度与主管授权，服务端查询必须同时满足组织、校区、业务线、学段范围。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、权限服务、全业务查询。
- **页面：** `platform.page-04`（权限中心）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `sys_role_school_stage_scope`，课程/班级/合同项/课消事实持久化标准 school_stage。
- **后端：** 把 schoolStage 作为数据范围维度，与组织、校区、业务线同时求交集后下推到查询。
- **前端：** 权限中心增加小学/初中/高中授权；列表筛选只展示授权学段。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:003-field-permissions`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/roles/{id}/data-scope` | `view` | path: id | schoolStages[] 与其他范围 |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/platform-foundation/roles/{id}/data-scope` | `edit` | expectedVersion,schoolStages[] 与其他范围 | scopeVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/me/effective-data-scope` | `view` | resourceKey | effectiveOrgIds,effectiveCampusIds,effectiveBusinessLineIds,effectiveSchoolStages |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：校区端/集团端主管分别授权小学和中学，所有列表、详情、Lookup、导出和接口伪造 ID 均按交集阻断。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERM-04 · 班课/个性化老师 T 级及计算方式不同，人员不能共用一个 T 级

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前 HR 没有教师业务线 T 级档案，只有岗位类别、教学资格、通用绩效规则。
- **闭环目标：** 建立 `TeacherLevelAssignment`：教师+业务线+学段+生效期+级别；课酬规则按该关系匹配。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR/课酬。
- **页面：** `hr.page-10`（教师资质）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `hr_teacher_level_assignment`，排除同教师同业务线同学段重叠生效区间。
- **后端：** 建立教师+业务线+学段+生效期的多条 T 级关系，课酬按授课发生日匹配唯一有效等级。
- **前端：** 教师资质页增加 T 级档案子表和历史版本；员工主表不再提供单值 T 级。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:018-teacher-subject`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/teacher-level-assignments` | `view` | teacherId,businessLineId,schoolStage,asOf,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/teacher-level-assignments` | `create` | teacherId,businessLineId,schoolStage,levelCode,effectiveFrom,effectiveTo,idempotencyKey | assignmentId,version |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；teacherId,businessLineId,schoolStage,occurredAt | assignmentId,levelCode,ruleEvidence |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一教师班课/个性化不同 T 级并存；重叠区间返回 409；历史课次解析到发生日版本。
- **前置依赖：** PAY-01、PAY-03。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERM-05 · 导出限制 5–15 万，暑寒假单月扣费约 30 万；需管理员授权特定人员不限导出

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 通用导出有审批和数据范围，HR 员工导出硬限制 5 万；未实现按资源/角色配置阈值和临时无限额授权。
- **闭环目标：** 配置资源级阈值、分片异步导出、临时授权有效期、双人审批、水印和下载次数；禁止简单移除上限。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、审批中心、安全合规、导出中心。
- **页面：** `platform.page-07`（文件与导入导出）、`security-compliance.page-05`（导出与水印）、`security-compliance.page-08`（临时授权与越权治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：扩展 `sec_export_approval`，新增 `sec_export_policy`、`sec_temporary_export_grant`、导出分片及下载审计。
- **后端：** 按资源配置同步/异步阈值，超过阈值分片导出；临时不限额授权必须双人审批、带有效期、水印和下载次数。
- **前端：** 导出前展示预计行数和审批要求；授权页显示有效期、剩余下载次数并支持撤销。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/security/SecurityComplianceController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/security/SecurityComplianceModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/security/SecurityComplianceService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/runtime/RuntimeFoundationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/securityComplianceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/runtimeFoundation.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:006-file-center`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/security-compliance-admin/export-policies` | `view` | resourceKey,campusId | rowLimit,asyncThreshold,hardLimit,approvalMode |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/security-compliance-admin/export-grants` | `export` | resourceKey,userId,validFrom,validTo,maxDownloads,reason,idempotencyKey | grantId,approvalId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/admin/runtime/export-tasks` | `export` | resourceKey,filters,columns,grantId?,idempotencyKey | taskId,estimatedRows,approvalRequired,chunkCount |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：5万/15万/30万边界、无授权、过期授权、下载次数用尽和双人审批全部覆盖，文件水印可追溯。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TARGET-01 · 预算区分新增、转介绍、扩科；同时设金额和人头/人次目标

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 未发现经营目标领域模型和自动完成率服务，现有“目标设置”主要是资源页/泛化配置。
- **闭环目标：** 建立目标主表、维度、版本和分解表，指标分别支持金额、人头、人次并绑定签单类型。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `platform.page-10`（目标、公立学校与年级配置）、`bi-dashboard.page-10`（指标口径与目标）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `bi_target_plan`、`bi_target_version`、`bi_target_line`，valueType 限 AMOUNT/HEADCOUNT/PERSON_TIME。
- **后端：** 建立目标主表、版本、维度和分解项，金额、人头、人次目标按签单类型分别保存。
- **前端：** 目标编制页支持新增/转介绍/扩科维度和多计量单位，发布前校验维度完整性。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:024-target-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/target-plans` | `create` | name,periodType,periodFrom,periodTo,dimensions[],ownerOrgId | targetPlanId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/target-plans/{id}/lines` | `create` | signingType,valueType,targetValue,dimensionValues,idempotencyKey | targetLineId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/target-plans/{id}/publish` | `execute` | expectedVersion | publishedVersion,lineCount,totalByValueType |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：金额、人头、人次三种单位不能混加；发布后目标行按签单类型和维度完整下钻。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TARGET-02 · 课消收入需排除引流、社团等课程类别

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** BI 有课消小时指标，但没有按课程经营分类计算课消收入和排除规则。
- **闭环目标：** 建立课程经营分类与收入单价快照，课消收入按有效扣费记录计算，并配置包含/排除类别。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教务/BI。
- **页面：** `platform.page-10`（目标、公立学校与年级配置）、`bi-dashboard.page-05`（合同收款与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：课程经营分类增加 includeInConsumptionRevenue；扣费事实保存 unitPriceSnapshot、categoryId/version。
- **后端：** 课程经营分类定义课消收入是否计入，课消收入读取成功未冲正扣费事实和发生时单价快照。
- **前端：** 目标和报表配置页展示包含/排除类别；下钻显示被排除事实及原因。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:024-target-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/course-business-categories` | `view` | status,keyword,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/academic-admin/course-business-categories/{id}` | `edit` | expectedVersion,includeInConsumptionRevenue,effectiveFrom | categoryVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/metrics/consumption-revenue` | `view` | period,campusIds[],productLineIds[],categoryIds[] | amount,includedCount,excludedCount,queryToken |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：引流、社团类别排除；常规课程计入；冲正扣费不计，收入明细合计等于总额。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TARGET-03 · 预算和课消按产品线拆分，如语数英物化、1v1、高中班课

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前没有统一产品线维表，也未进入目标和 BI 计算。
- **闭环目标：** 增加产品线层级和课程映射版本；目标、合同、课消、退款、业绩和 BI 全部引用同一产品线 ID。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、全业务数据。
- **页面：** `platform.page-10`（目标、公立学校与年级配置）、`academic.page-02`（课程产品）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `base_product_line`、`academic_course_product_line_version`，业务事实补 product_line_id/version 快照。
- **后端：** 建立分层产品线及课程映射版本，目标、合同、课消、退款、业绩和 BI 统一保存 productLineId。
- **前端：** 产品线配置支持树形维护和课程映射；业务筛选统一使用远程多选。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:024-target-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/product-lines` | `view` | parentId,status,keyword,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/product-lines` | `create` | parentId,code,name,level,sortNo | productLineId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/courses/{id}/product-line-assignments` | `create` | productLineId,effectiveFrom,effectiveTo,idempotencyKey | assignmentId,version |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：语数英物化、1v1、高中班课映射可版本回放；跨模块 API 返回同一 productLineId。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TARGET-04 · 集团目标自动分解到项目/产品线并计算完成率

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 未发现目标分解、权重、调整审批和完成率计算器。
- **闭环目标：** 实现集团→区域→校区→部门/项目/产品线分解，保留分解版本、调整原因、锁定和进度快照。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `platform.page-10`（目标、公立学校与年级配置）、`bi-dashboard.page-10`（指标口径与目标）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V97__xuebang_scope_and_target_governance.sql。领域数据方案：新增 `bi_target_allocation`、`bi_target_adjustment`、`bi_target_progress_snapshot`，父子目标合计约束按 valueType 校验。
- **后端：** 实现集团→区域→校区→部门/项目/产品线逐级分解、锁定、调整和完成率快照。
- **前端：** 目标分解页使用树形表格，显示未分配差额、锁定状态、调整原因和完成率。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/authz/DataScopeService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `base:024-target-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/target-plans/{id}/allocate` | `create` | parentLineId,allocations[{dimensionValues,targetValue}],idempotencyKey | allocationVersion,unallocatedValue |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/target-plans/{id}/lock` | `create` | expectedVersion,scopeNodeId | lockedNodes[],version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/target-plans/{id}/progress` | `view` | asOf,dimensionLevel | nodes[{target,actual,completionRate}],snapshotVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：每层子目标合计等于父目标；锁定后直接修改返回 409；调整单审批后生成新版本并保留旧快照。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 8.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-permissions-targets): close PERM-01,PERM-02,PERM-03,PERM-04,PERM-05,TARGET-01,TARGET-02,TARGET-03,TARGET-04`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 9: Stage 3 · 客户管理（CRM-01、CRM-02、CRM-03、CRM-04、CRM-05、CRM-06、CRM-07）

**依赖：** ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V98__xuebang_crm_and_student_operations.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 9.1：逐项差异确认与复用决策

1. 对本域 7 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 9.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V98__xuebang_crm_and_student_operations.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 9.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 9.4：逐项接口级方案

##### CRM-01 · 记录客户被分配次数、最后分配人和日期

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** `crm_assignment_log` 已保留每次分配的原负责人、新负责人、分配人和时间；客户/线索列表尚未聚合展示“分配次数、最后分配”。
- **闭环目标：** 在 CRM 查询投影增加 `assignmentCount`、`lastAssignedBy`、`lastAssignedAt`，支持筛选和导出。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）、`crm.page-06`（分配与归属）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：复用客户分配事件，补 `(customer_id, assigned_at desc)` 索引并为客户查询投影增加三个字段。
- **后端：** 在客户列表投影和分配事件台账中计算分配次数、最后分配人和最后分配时间。
- **前端：** 客户列表增加分配次数/最后分配人/日期列、筛选和导出；点击次数打开事件时间线。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/crm-admin/customers` | `view` | keyword, assignmentCountMin, lastAssignedById, lastAssignedFrom, lastAssignedTo, page, pageSize | records 含 assignmentCount,lastAssignedById,lastAssignedByName,lastAssignedAt |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/customers/{id}/assignment-events` | `view` | page, pageSize | records, total |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters, columns[], idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：构造三次分配事件，断言列表、详情和导出均返回 3 及最新人员/日期。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-02 · 客户可释放或转到其他校区；A 校登记、B 校成交时一线或行政可处理

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 同校区公海释放/领取已闭环；正式学员售后转校已闭环。售前客户跨校区迁移没有可执行接口，CRM 还会拒绝直接跨校区分配。
- **闭环目标：** 新增“客户跨校区移交单”，包含原/目标校区、归属人、渠道和业绩归因；审批后迁移客户、线索和跟进任务，合同继续引用稳定客户 ID。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、员工端接口、后端 API、审批中心、PostgreSQL。
- **页面：** `crm.page-06`（分配与归属）、`crm.page-03`（客户档案）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `crm_customer_transfer`、`crm_customer_transfer_item` 和归属前后快照；状态 DRAFT→SUBMITTED→APPROVED→EXECUTED/REJECTED/FAILED。
- **后端：** 新增客户跨校区移交状态机，审批通过后事务性更新客户归属、线索归属和未完成跟进任务，合同仍引用稳定客户 ID。
- **前端：** 分配与归属页新增跨校移交；客户详情显示历史交接和目标校只读/服务权限。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:019-customer-owners`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customer-transfers/preview` | `view` | customerId, targetCampusId, targetOwnerId, attributionPolicy | affectedLeads[],followups[],contracts[],permissionImpact |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customer-transfers` | `create` | customerId, sourceCampusId, targetCampusId, targetOwnerId, attributionPolicy, reason, idempotencyKey | transferId, approvalId, status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customer-transfers/{id}/execute` | `execute` | expectedVersion, approvalId, idempotencyKey | status, movedEntities[], retainedContracts[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖 A 校登记/B 校成交、审批拒绝、执行中单项失败回滚和合同稳定 ID 不变。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-03 · 班课资源一个月未成交后自动提醒个性化部门跟进

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 公海规则存在 `inactive_recycle_hours` 字段，但未发现定时扫描和班课转个性化任务执行器。
- **闭环目标：** 增加资源沉默规则、业务线标签、定时任务、提醒/转派状态机和防重复幂等键。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、调度器、消息中心、PostgreSQL。
- **页面：** `crm.page-04`（跟进与公海）、`crm.page-06`（分配与归属）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `crm_silence_rule`、`crm_silence_hit`、`crm_followup_transfer_task`；唯一键 `(rule_version_id, customer_id, silence_window_end)`。
- **后端：** 新增资源沉默规则与扫描任务，命中一个月未成交的班课资源后生成个性化部门提醒/转派任务，使用规则版本和业务幂等键去重。
- **前端：** 跟进与公海页增加沉默队列，支持确认、转派、忽略及原因；规则在平台配置页版本化发布。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:003-follow-tasks`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/silence-rules` | `create` | name, sourceBusinessLineId, targetBusinessLineId, inactiveDays, customerCategories[], effectiveFrom | ruleId, version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/silence-rules/scan` | `create` | asOfDate, ruleIds[], idempotencyKey | jobId, candidateCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/silence-tasks/{id}/transfer` | `create` | targetOwnerId, expectedVersion, reason, idempotencyKey | taskStatus, assignmentEventId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一规则同一窗口重复扫描只生成一个任务；成交、C 类无效和已转派客户不再生成。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-04 · 转化率=期间转化成功÷（期间新增+期间再分配），不含 C 类无效客户

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前 CRM 看板按“已签约线索÷全部线索”计算，不符合指定分母，也没有 C 类排除。
- **闭环目标：** 新建版本化 `CRM_CONVERSION_RATE` 指标，明确新增事件、再分配事件、成功事件和无效分类，以事件时间而非当前状态计算。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `crm.page-09`（销售漏斗与效能）、`bi-dashboard.page-03`（招生 CRM 分析）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 CRM 事件指标贡献表，保存 eventId、customerId、eventType、occurredAt、classificationAtEvent 和 metricVersion。
- **后端：** 实现版本化 CRM_CONVERSION_RATE 事件指标，按期间新增事件+再分配事件为分母、成功事件为分子并排除 C 类无效。
- **前端：** 漏斗与效能页展示公式、分子/分母、排除数及逐笔下钻，禁止用当前客户状态回算历史。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:006-sales-funnel`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/metrics/conversion-rate` | `view` | periodFrom, periodTo, campusIds[], businessLineIds[], metricVersionId | numerator, denominator, rate, excludedCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/metrics/conversion-rate/contributions` | `view` | queryToken, contributionType, page, pageSize | records, total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/metrics/conversion-rate/rebuild` | `execute` | periodFrom, periodTo, metricVersionId, idempotencyKey | jobId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：用新增、再分配、成功、C 类无效四类事件固定算例断言结果和事件时间边界。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-05 · 客户列表除首单日期、课程、金额外增加首单合同 ID

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 客户列表没有连接合同取得首单合同 ID，也没有首单投影。
- **闭环目标：** 以 `canonical_student_id/customer_id` 关联首次支付并盖章合同，增加首单合同 ID/编号/日期/课程/实收列。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增/刷新 `crm_customer_first_contract_projection`，保存合同 UUID、编号、日期、课程项和实收；退款/撤销后按规则重算。
- **后端：** 按稳定 customerId/canonicalStudentId 找到首次有效支付且已盖章合同，形成首单合同投影。
- **前端：** 客户列表增加首单合同 ID/编号/日期/课程/实收列，合同 ID 可复制并跳转合同详情。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/crm-admin/customers` | `view` | firstContractFrom, firstContractTo, firstCourseIds[], page, pageSize | records 含 firstContractId,firstContractNo,firstContractAt,firstCourseNames,firstPaidAmount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/customers/{id}/first-contract` | `view` | path: id | contractId,contractNo,contractItems[],paidAmount,evidencePayments[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customers/{id}/first-contract/rebuild` | `execute` | reason, idempotencyKey | projectionVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖已盖章已支付、未盖章、撤销、全退和多孩子家庭，首单关系不串档。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-06 · 手机号唯一导致老资源/渠道冲突；需激活老资源并记录后期获资部门绩效

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 家庭/家长去重、客户合并恢复、营销归因已实现；“再次获资部门”与最终业绩的双归因没有实现。
- **闭环目标：** 保留原始获客和本次激活两条归因，配置首获/激活/成交贡献比例，并在业绩结算时引用归因版本。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、员工端接口、后端 API、PostgreSQL、业绩引擎。
- **页面：** `crm.page-07`（去重合并与撞单）、`crm.page-09`（销售漏斗与效能）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `crm_acquisition_attribution`、`crm_reactivation_event`、`crm_attribution_rule_version`，手机号只用于候选查重。
- **后端：** 把首次获客、再次激活和成交归因拆为独立事件，归因比例版本化并由业绩结算引用。
- **前端：** 撞单页显示原始获客与本次激活证据；激活需选择部门/人员并展示预计贡献比例。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:014-lead-duplicates`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customers/{id}/reactivations/preview` | `view` | channelId, departmentId, ownerId, occurredAt | duplicateCandidates[], attributionPreview[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/customers/{id}/reactivations` | `create` | channelId, departmentId, ownerId, occurredAt, evidence, idempotencyKey | reactivationId, attributionVersionId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/customers/{id}/attributions` | `view` | asOf, page, pageSize | records, total, activeRuleVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：老资源激活后保留原获客事件，业绩贡献按版本比例合计 100%，重复提交不重复记功。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CRM-07 · 一个家长多个孩子，同一手机号不应阻止多个孩子报名

- **补充 PRD 基线：** 明确覆盖；✅ 已解决。
- **最新主分支证据：** 新平台采用家庭—家长—孩子模型；同一 guardian 手机号可关联多个 `crm_customer/stu_student`，正式学员用 UUID，不以手机号合并。CRM 关系测试已覆盖。
- **闭环目标：** 上线迁移时按“家长手机号+孩子身份”建立家庭关系，并对疑似误合并数据生成复核清单。
- **实施模式：** `REUSE_EXISTING`。
- **改动端：** 数据迁移、CRM、家长关系。
- **页面：** `crm.page-07`（去重合并与撞单）、`parent-service.page-02`（家长账号与学员绑定）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：复用 `crm_customer_family`、`crm_guardian` 和稳定学员 UUID；只在历史迁移临时区保存源行、匹配结果和异常，不增加永久手机号解析表。
- **后端：** 保持现有家庭—监护人—多孩子模型，不按手机号合并正式学员；迁移工具按家长手机号加孩子身份建立关系并输出疑似误合并清单。
- **前端：** 复用现有家庭和学员绑定入口；只有迁移异常需要人工处置时，使用已有数据修复/异常治理入口展示清单。
- **候选落地文件：** 后端 无默认改动；前端 无默认改动；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:014-lead-duplicates`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

- **接口级决策：** 无默认新增接口；迁移异常通过现有数据修复任务和 CRM 关系能力处理。

- **最小验证：** 在现有 CRM 关系测试中补一条同手机号绑定两个不同孩子的回归；迁移时核对异常清单，不新建整域 E2E。
- **前置依赖：** ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 9.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-crm): close CRM-01,CRM-02,CRM-03,CRM-04,CRM-05,CRM-06,CRM-07`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 10: Stage 3 · 学员管理（STU-01、STU-02、STU-03、STU-04、STU-05）

**依赖：** CRM、ID、COURSE。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V98__xuebang_crm_and_student_operations.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 10.1：逐项差异确认与复用决策

1. 对本域 5 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 10.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V98__xuebang_crm_and_student_operations.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 10.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 10.4：逐项接口级方案

##### STU-01 · 学员列表快速检索在读、停课、沉默人数

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 有学员状态和课程权益状态，但当前没有完整的管理端学员列表及“在读/停课/沉默”统一口径和快捷统计。
- **闭环目标：** 定义三类互斥/可叠加状态，建立学员运营快照，列表顶部显示数量卡片并支持点击筛选。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）、`after-sales.page-01`（服务工作台）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `stu_operation_status_snapshot`，保存 asOfDate、studentId、active/paused/silent 标志及证据 ID。
- **后端：** 定义在读、停课、沉默运营状态并构建日快照；状态允许按规则叠加但计数卡使用明确去重口径。
- **前端：** 学员列表顶部展示三类数量卡，点击后提交相应筛选并显示口径日期。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/operation-summary` | `view` | asOfDate, campusIds[], businessLineIds[] | activeCount,pausedCount,silentCount,overlapMatrix |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students` | `view` | operationStatuses[], asOfDate, page, pageSize | records,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{id}/operation-status-evidence` | `view` | asOfDate | statuses[], evidenceEvents[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：固定日期夹具逐人断言三个状态及重叠矩阵，数量卡、列表和导出使用同一快照。
- **前置依赖：** CRM、ID、COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### STU-02 · 快速区分班课与个性化学员人数

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 课程和班级有 `course_type/class_type`，但没有按学员当前有效权益聚合班课/1v1/1vN人数。
- **闭环目标：** 以有效权益+期间课消形成业务线标签，支持同一学员同时属于多业务线并分别计人头。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）、`bi-dashboard.page-05`（合同收款与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `stu_business_line_snapshot`，唯一键 `(snapshot_date, student_id, business_line_id)`，保存权益/课消证据。
- **后端：** 以有效权益和期间课消生成学员业务线标签，同一学员可同时属于班课和个性化并在各线分别计人头。
- **前端：** 学员列表增加业务线多选和人头卡；双归属学员展示多个标签。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/business-line-summary` | `view` | periodFrom,periodTo,campusIds[] | lines[{businessLineId,studentCount}],multiLineStudentCount |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students` | `view` | businessLineIds[],periodFrom,periodTo,page,pageSize | records 含 businessLines[], total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{id}/business-line-evidence` | `view` | periodFrom,periodTo | entitlements[],consumptions[],businessLines[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员同时有两条业务线权益时各线计 1、全局去重计 1，证据可下钻。
- **前置依赖：** CRM、ID、COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### STU-03 · 重点生源校统计分析

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 正式学员主档未形成可分析的公立学校维表和重点生源校指标。
- **闭环目标：** 增加学校标准库、别名合并、学员学校历史、校区覆盖关系；按新增、到访、签约、实收、在读、续费分析。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `crm.page-03`（客户档案）、`bi-dashboard.page-03`（招生 CRM 分析）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `base_public_school`、`base_public_school_alias`、`stu_school_history`、`base_campus_school_coverage`。
- **后端：** 建立公立学校标准库、别名归并、学员学校历史和校区覆盖关系，提供新增到续费全漏斗。
- **前端：** 学员编辑使用学校远程选择器；重点生源校分析支持别名合并和漏斗下钻。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/platform/PlatformFoundationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/platformFoundation.ts`、`SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/platform-foundation/public-schools` | `view` | keyword,regionId,stage,status,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/platform-foundation/public-schools/{id}/aliases` | `create` | alias,source,effectiveFrom | aliasId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/public-school-analysis` | `view` | period,campusIds[],schoolIds[],stages[] | newLeads,visits,contracts,paidAmount,activeStudents,renewals,records |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：别名数据归并到同一 schoolId；学员转学按发生日匹配历史学校；漏斗逐级可回到稳定客户/学员 ID。
- **前置依赖：** CRM、ID、COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### STU-04 · 最近一次签约日期及最近三个月签约学员筛选

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 合同有支付时间，但学员查询未聚合 `lastContractAt`，也没有最近三个月筛选。
- **闭环目标：** 建立学员合同摘要投影，按最后有效签约/支付时间筛选，退费合同是否计入需在指标口径中明确。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `stu_contract_summary_projection`，保存 lastContractId、lastSignedAt、lastPaidAt、refundDisposition 和 projectionVersion。
- **后端：** 建立最后有效签约/支付投影，支持最近签约日期和最近三个月筛选，退款合同是否计入由指标版本配置。
- **前端：** 学员列表增加最近签约日期列与快捷筛选“近 3 个月”，口径说明展示采用签约还是支付日期。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students` | `view` | lastContractFrom,lastContractTo,lastContractBasis,refundDisposition,page,pageSize | records 含 lastContractId,lastSignedAt,lastPaidAt |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{id}/contract-summary` | `view` | basis,asOf | lastContract,contracts[],projectionVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/student-contract-projections/rebuild` | `execute` | studentIds[],periodFrom,idempotencyKey | jobId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖签约未支付、支付、部分退、全退和三个月边界，筛选结果与投影证据一致。
- **前置依赖：** CRM、ID、COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### STU-05 · 自动生成每个学员的购课、课消、服务顾问和老师轨迹

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 合同、权益、班级关系、课次、考勤、服务归属、转校/转课均有稳定 ID；尚无统一学员时间轴查询和页面。
- **闭环目标：** 新增 Student 360 时间轴读模型，统一事件类型、时间、操作者、前后值、原始单据链接和权限脱敏。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、家长/员工端只读接口、后端 API、PostgreSQL。
- **页面：** `crm.page-03`（客户档案）、`after-sales.page-01`（服务工作台）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V98__xuebang_crm_and_student_operations.sql。领域数据方案：新增 `stu_timeline_event` 投影，保存 eventType、occurredAt、actorId、before/after snapshot、sourceType/sourceId 和 visibility。
- **后端：** 构建 Student 360 统一事件时间线，汇聚购课、课消、服务顾问、教师、转课、退款等事件并按字段权限脱敏。
- **前端：** 学员详情新增时间线页签、事件类型筛选、原始单据跳转和脱敏提示。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{id}/timeline` | `view` | eventTypes[],from,to,page,pageSize | records,total,nextCursor |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{studentId}/timeline/{eventId}` | `view` | path ids | event, beforeSnapshot, afterSnapshot, sourceLink, audit |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/student-timeline/rebuild` | `execute` | studentIds[],eventTypes[],idempotencyKey | jobId,acceptedCount |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：固定学员完成购课→课消→转课→退款链路，时间线顺序、来源链接、前后值和字段脱敏全部正确。
- **前置依赖：** CRM、ID、COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 10.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-students): close STU-01,STU-02,STU-03,STU-04,STU-05`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 11: Stage 4 · 优惠管理（DISC-01、DISC-02、DISC-03、DISC-04、DISC-05、DISC-06、DISC-07）

**依赖：** COURSE。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V99__xuebang_discount_rule_engine_v2.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 11.1：逐项差异确认与复用决策

1. 对本域 7 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 11.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V99__xuebang_discount_rule_engine_v2.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 11.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 11.4：逐项接口级方案

##### DISC-01 · 优惠类型只有满减、直减、折扣，过于简单

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 数据模型支持满减、比例、组合、多科、转介绍、额外审批；计算实现实际上只区分比例与固定金额，适用课程/人数/季节等条件不足。
- **闭环目标：** 将规则拆成条件、动作、叠加策略和分摊策略；增加赠课、套餐、阶梯、N 科减免、指定科目减免等动作。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、员工制单端接口、后端 API、PostgreSQL。
- **页面：** `contract.page-02`（优惠规则）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：扩展 `contract_discount_rule` 为版本主表，新增条件/动作/范围/组合四类结构化子表，禁止用无约束 JSON 作为结算真值。
- **后端：** 将优惠规则拆成条件、动作、叠加策略和分摊策略，支持赠课、套餐、阶梯、N 科减免和指定科目减免。
- **前端：** 优惠规则页使用结构化规则设计器和模拟器，发布前展示冲突与影响范围。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-rules/{id}/versions` | `create` | conditions[],actions[],stackPolicy,allocationPolicy,scope,effectiveFrom | ruleVersionId,validation |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-rules/simulate` | `view` | ruleVersionId,quoteInput | matched,actions[],allocations[],explanations[] |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/discount-rules/{id}/publish` | `execute` | expectedVersion,ruleVersionId | publishedVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：每种新增动作至少一个命中和一个不命中用例，规则版本重放金额完全一致。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-02 · 暑秋多科连报不能自动直减

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 有“暑期多科联报”种子规则，但报价只接收单个价格方案和手选规则，不能根据课程组合自动命中。
- **闭环目标：** 报价输入课程项数组，规则引擎自动返回可用/已命中/冲突优惠并说明原因。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 员工制单端接口、管理端 PC、后端 API。
- **页面：** `contract.page-02`（优惠规则）、`contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：不新增独立事实表，报价快照保存 season、subjectCount、候选/命中/冲突规则版本。
- **后端：** 暑秋多科报价统一传课程项数组，由规则引擎返回可用、命中和冲突优惠并说明原因。
- **前端：** 员工制单选择多科后自动请求匹配，但最终金额只以服务端确认报价为准。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-rules/match` | `create` | campusId,studentId,season,courseItems[],occurredAt | availableRules[],matchedRules[],conflicts[],recommendedCombination |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | courseItems[],discountRuleIds[],season | courseItems[].discountAllocations,totalDiscount,explanations |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：暑秋两科、三科及跨季节用例验证自动直减和不适用原因。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-03 · 财务设固定直减供校区自选，容易选错或凑单

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 已有发布状态、有效期、门槛、叠加和额外审批，但缺课程范围、校区范围、学员资格和自动匹配校验。
- **闭环目标：** 规则必须配置适用范围；前端只展示满足条件的优惠，服务端再次校验并对异常凑单触发审批。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 员工制单端接口、管理端 PC、后端 API、审批中心。
- **页面：** `contract.page-02`（优惠规则）、`contract.page-06`（额外折扣审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：规则范围表增加范围交集索引；报价快照保存 eligibilityEvidence。
- **后端：** 优惠规则必须声明校区、课程、学段、时间和客户范围，员工端只展示合法优惠，异常凑单提交额外折扣审批。
- **前端：** 选择器仅展示满足条件的规则，手工搜索越界规则显示禁用原因；强制应用进入审批。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/contract-admin/discount-rules` | `view` | keyword,campusId,courseIds[],schoolStage,occurredAt,eligibleOnly,page,pageSize | records,total,disabledReasons |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-rules/{id}/eligibility-check` | `create` | customerId,courseItems[],campusId,occurredAt | eligible,reasons[],approvalRequired |
| 新增 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/extra-discounts` | `create` | quoteId,requestedRuleId,reason,evidence,idempotencyKey | applicationId,approvalId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：合法范围自动可选，越界直接提交被阻断；审批通过后使用审批快照而非绕过规则。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-04 · 只能单选，不能“先比例折扣、再直减”等整单叠加

- **补充 PRD 基线：** 明确覆盖；✅ 已解决。
- **最新主分支证据：** 后端 `discountRuleIds` 支持多选、去重、优先级、可叠加校验和逐条应用，并保存报价快照；合同关系测试已覆盖多优惠。
- **闭环目标：** 员工端必须统一使用多选报价接口；补一条端到端用例验证“折扣→直减”的金额和快照。
- **实施模式：** `REUSE_EXISTING`。
- **改动端：** 员工制单端、管理端 PC、后端 API。
- **页面：** `contract.page-05`（员工端制单审计）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：复用 `contract_quote_snapshot` 和 `contract_order_discount_rule`；只有业务明确要求展示逐步金额时才扩展现有快照 JSON，不新建报价确认表。
- **后端：** 复用现有 `discountRuleIds[]`、优先级、可叠加校验和下单报价快照；不新增二次报价确认状态机。
- **前端：** 确认员工制单入口传递 `discountRuleIds[]` 并展示规则顺序；若当前入口已满足则不改管理端。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | pricePlanId,discountRuleIds[] | originalAmount,discountAmount,finalAmount,rules[] |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/contract-admin/staff-order-records` | `create` | 现有 StaffOrderRequest，包含 discountRuleIds[] | orderId,quoteSnapshotId |

- **最小验证：** 扩展现有合同关系测试，增加“比例折扣后直减”的固定算例并校验报价快照；仅在员工制单 UI 实际修改时补一个交互用例。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-05 · 自动优惠难，校区需逐学员向财务申请配置

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 现有规则仍需手工选择；不存在按课程组合和学员资格自动匹配的执行器。
- **闭环目标：** 增加规则匹配 API，自动应用无需审批的最优合法组合；只把超范围优惠送审批。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 员工制单端、管理端 PC、后端 API、审批中心。
- **页面：** `contract.page-05`（员工端制单审计）、`contract.page-06`（额外折扣审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：新增规则组合评分配置，报价保存 selectionStrategyVersion 和候选组合比较。
- **后端：** 规则匹配服务自动选择最优合法组合，无需审批的直接应用，超范围或互斥冲突才生成审批。
- **前端：** 制单页默认展示推荐组合、节省额和未选原因，允许在合法组合间切换。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-combinations/recommend` | `create` | quoteInput,optimizationGoal | recommended,alternatives[],rejected[],strategyVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/discount-combinations/{combinationId}/apply` | `create` | quoteDraftId,idempotencyKey | quoteId,approvalRequired,approvalId? |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：合法最优组合无需审批；超范围组合自动生成审批；同输入推荐结果确定性一致。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-06 · 退费时优惠应自动降档并扣回已课消折扣

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 退款只校验金额不超过实付，并把绩效同步生成 `PENDING` 任务；未重新执行优惠规则或计算应扣差额。
- **闭环目标：** 退款试算应重放原报价快照，按退后课程组合重新定档，计算原优惠、应享优惠、已消耗权益和需扣回金额。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、退款引擎。
- **页面：** `contract.page-02`（优惠规则）、`contract.page-08`（合同变更与退转）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：新增优惠重放结果与 refundItem 关联，保存 originalEntitlement、remainingCombination、repricedAmount 和 clawback。
- **后端：** 退款试算重放原报价快照，按退后课程组合重新定档并计算应扣回优惠。
- **前端：** 退款明细展示原优惠、退后应享、已消耗、优惠扣回及逐课程分摊。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/quotes/{quoteId}/refund-replay` | `create` | refundCourseItems[],consumedEntitlements[],refundOccurredAt | originalDiscount,recalculatedDiscount,clawback,courseAllocations[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/quotes/{quoteId}/refund-replays/{replayId}` | `view` | path ids | inputs,ruleVersions,steps,allocations,clawback |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：多科退一科、已课消、部分退款和规则版本已停用场景均按原版本重放。
- **前置依赖：** REFUND-04。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### DISC-07 · 优惠多时不能按名称检索

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 后端优惠列表支持按规则名称/编号搜索；当前合同管理前端未提供搜索框，报价选择器只是加载全部规则。
- **闭环目标：** 在管理页和员工端选择器接入远程搜索、状态/类型筛选和分页，禁用规则仅历史回显。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、员工制单端、后端 API。
- **页面：** `contract.page-02`（优惠规则）、`contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V99__xuebang_discount_rule_engine_v2.sql。领域数据方案：为规则名称、编码、状态、类型、生效期建立组合/全文索引。
- **后端：** 优惠管理和选择器接入服务端分页远程搜索、类型/状态筛选，已停用规则仅支持历史回显。
- **前端：** 优惠选择器使用 DinuoEntityPicker 多选，300ms 防抖、分页和已停用标签。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoEntityPicker.vue`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/contract-admin/discount-rules` | `view` | keyword,ruleTypes[],statuses[],campusId,courseIds[],page,pageSize | records,total |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；ids[] | items[{id,code,name,status,disabled}] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：一万条规则夹具下名称/编号搜索正确、分页稳定，停用规则不可新选但历史 ID 可回显。
- **前置依赖：** COURSE。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 11.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-discounts): close DISC-01,DISC-02,DISC-03,DISC-04,DISC-05,DISC-06,DISC-07`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 12: Stage 4 · 合同管理（CON-01、CON-02、CON-03、CON-04、CON-05、CON-06、CON-07、CON-08）

**依赖：** CRM、COURSE、DISC。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V100__xuebang_contract_item_and_performance_allocation.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 12.1：逐项差异确认与复用决策

1. 对本域 8 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 12.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V100__xuebang_contract_item_and_performance_allocation.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 12.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 12.4：逐项接口级方案

##### CON-01 · 合同多项优惠不能多选

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 服务端订单请求支持多条优惠，报价页也支持多选；当前管理端模拟制单表单仍只有单个优惠字段，员工端实际交互未完成本次核验。
- **闭环目标：** 统一所有制单端为 `discountRuleIds[]`，移除单值兼容字段，端到端验收报价与合同快照一致。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 员工制单端、管理端 PC、后端 API。
- **页面：** `contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：合同订单记录 quoteSnapshotId 和多优惠关联，迁移旧 discountRuleId 为单元素关联。
- **后端：** 统一制单契约为 discountRuleIds[] 并移除单值字段的写入兼容，合同只能由已确认报价快照生成。
- **前端：** 制单页多选优惠并展示叠加明细，提交前比对 quoteHash 防止金额漂移。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/staff-order-records` | `create` | customerId,courseItems[],discountRuleIds[],quoteSnapshotId,quoteHash,idempotencyKey | orderId,contractItems[],discountRules[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/orders/{id}/discounts` | `view` | path: id | rules[],steps[],allocations[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：API 提交单值字段返回 400；多选报价、合同快照和订单优惠关联完全一致。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-02 · 优惠多时不能筛选

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 后端支持名称搜索，当前专用前端未接搜索/类型/状态筛选。
- **闭环目标：** 使用分页远程选择器，支持名称、编号、类型、校区、课程和生效状态。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 员工制单端、管理端 PC、后端 API。
- **页面：** `contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：复用优惠组合索引和 resolve 接口，不在前端一次加载全量规则。
- **后端：** 制单优惠选择使用分页远程查询，支持名称、编号、类型、校区、课程和生效状态。
- **前端：** 选择器保留已选跨页项、显示适用范围和不可选原因。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/contract-admin/discount-rules` | `view` | keyword,ruleTypes[],campusId,courseIds[],effectiveAt,statuses[],page,pageSize | records,total |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；ids[] | items[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：跨页多选、输入搜索、课程变化后的重新校验及停用历史回显通过。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-03 · 课程价格小数导致购买课时为小数，不是整期

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 金额和课时均使用小数保存，没有“整期课时优先、金额差额按规则吸收”的产品政策。
- **闭环目标：** 价格方案显式区分“按期定价”和“按课时定价”；按期方案锁定整数课时和总价，不反推小数课时。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工制单端、后端 API、PostgreSQL。
- **页面：** `contract.page-01`（价格与套餐）、`contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：价格方案增加 pricingMode、termHours、totalPrice、unitPriceScale、roundingMode、roundingAdjustmentAccount。
- **后端：** 价格方案显式区分 TERM 和 HOURLY；按期方案锁定整数课时与总价，不由小数单价反推课时。
- **前端：** 价格方案编辑器按模式显示字段；报价明细标识舍入和尾差。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:004-multi-subject-package`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/price-plans` | `create` | courseId,campusId,pricingMode,termHours,totalPrice,unitPrice,scale,roundingMode,effectiveFrom | pricePlanId,normalizedValues |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/price-plans/{id}/validate` | `view` | sampleQuantities[] | valid,calculationExamples[],errors[] |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | courseItems[] | courseItems[].hours,price,roundingAdjustment |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：按期方案课时必须整数；5959.2/5960 和 120/120.02 用例按配置舍入且总账平衡。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-04 · 转课结转时不能选择多张代金券和多个课程类型

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 售后转课只支持一个原权益、一个目标课程和一个目标班级；没有代金券集合和多课程结转。
- **闭环目标：** 建立结转试算单，支持多个来源权益/券、多个目标课程项，逐项分摊余额并保留来源链。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、PostgreSQL。
- **页面：** `contract.page-08`（合同变更与退转）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：新增 `contract_transfer_draft`、`contract_transfer_source`、`contract_transfer_target`、`contract_transfer_allocation`。
- **后端：** 建立多来源权益/代金券到多目标课程项的结转试算和分摊，保留来源链。
- **前端：** 合同变更页提供来源多选、目标课程表格、自动分摊和手工调整校验。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:007-refund-transfer`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/transfers/preview` | `view` | studentId,sourceEntitlementIds[],voucherIds[],targetCourseItems[] | sources[],targets[],allocations[],difference,warnings[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/transfers` | `create` | previewId,allocations[],reason,idempotencyKey | transferId,approvalId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/transfers/{id}/lineage` | `view` | path: id | sourceNodes[],allocationEdges[],targetNodes[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两张券+两份权益转三门课，分配合计、审批和执行后来源链无丢失。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-05 · 跨校区录合同后，校区无法关联跨校合同

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 新合同会强制学员、家长、课程价格、负责人和校区一致；售后转校保留原合同校区，不提供目标校区合同协作视图。
- **闭环目标：** 不放开无约束跨校写入；增加跨校授权视图、交接单和目标校区只读/服务权限，原合同与交接记录可追溯。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工端接口、后端 API、权限服务。
- **页面：** `contract.page-04`（合同订单台账）、`contract.page-08`（合同变更与退转）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：扩展 `crm_contract_handoff`，增加 accessLevel、validFrom/To、serviceOwnerId 和访问审计。
- **后端：** 增加跨校合同授权视图和交接单，目标校只获得经审批的只读/服务权限，不开放无约束跨校写入。
- **前端：** 合同台账显示跨校授权标识；目标校可查看并发起服务动作，合同财务字段仍按字段权限脱敏。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:001-quotation-orders`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/cross-campus-handoffs` | `create` | targetCampusId,serviceOwnerId,accessLevel,validTo,reason,idempotencyKey | handoffId,approvalId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/orders/cross-campus-visible` | `view` | campusId,accessLevel,status,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/cross-campus-handoffs/{id}/revoke` | `create` | expectedVersion,reason | status,revokedAt |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：A 校合同在 B 校按授权可见，B 校不能修改金额；授权过期/撤销后立即失效并保留审计。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-06 · 结课停课三个月后再报名，按老生召回政策应算新增

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 未发现“离读超过 N 天→召回新增”的签单类型自动判定。
- **闭环目标：** 建立签单类型规则，以最后有效课消/结课/停课日期判定召回，规则版本写入合同。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工制单端、后端 API。
- **页面：** `contract.page-05`（员工端制单审计）、`contract.page-08`（合同变更与退转）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：新增 `contract_signing_type_rule`、`contract_signing_type_evidence`，合同项保存判定结果和版本。
- **后端：** 建立签单类型判定服务，以最后有效课消/结课/停课日期和规则版本判断三个月后的召回是否为新增。
- **前端：** 制单页显示自动判定、证据日期和规则；人工纠错必须发起审批。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/signing-types/evaluate` | `view` | studentId,courseItems[],signedAt | signingType,ruleVersion,evidenceEvents[],confidence |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/signing-type-change-requests` | `create` | requestedType,reason,evidence,idempotencyKey | requestId,approvalId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/orders/{id}/signing-type-evidence` | `view` | path: id | type,ruleVersion,evidenceEvents[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：结课/停课未满与超过三个月边界、期间课消、退款后召回均有判定用例。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-07 · 多科收费按比例把每课程分给多人，实际通常每课程归一个业绩人

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 当前订单是一课程一负责人，尚未实现一个合同多课程及逐课程业绩人，因此不能覆盖该场景。
- **闭环目标：** 合同主表+课程项；课程项默认单一业绩人，只有明确审批时才允许课程内多人分摊。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 员工制单端、管理端 PC、后端 API、业绩引擎。
- **页面：** `contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：performance_allocation 增加 isPrimary、approvedMultiParty、approvalId，普通模式唯一主归属。
- **后端：** 课程项默认单一主业绩人；只有审批通过的课程项才能启用多人分摊。
- **前端：** 制单页每课程默认单选业绩人，选择多人时强制填写比例并进入审批。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/items/{itemId}/performance-owner` | `create` | employeeId,expectedVersion | allocationId,isPrimary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/items/{itemId}/multi-owner-requests` | `create` | allocations[{employeeId,ratio}],reason,idempotencyKey | requestId,approvalId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：普通课程项只能一个主业绩人；多人比例合计 100% 且无审批不能盖章。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### CON-08 · 同合同多科导致同课程出现三名提成人，财务需手工修正

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 没有逐课程归属唯一约束、归属审批和财务修正台账。
- **闭环目标：** 增加 `(contract_item_id, performance_role)` 唯一主归属；修正必须走审批并重算工资/业绩。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、业绩/工资引擎。
- **页面：** `contract.page-04`（合同订单台账）、`contract.page-09`（异常与对账）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V100__xuebang_contract_item_and_performance_allocation.sql。领域数据方案：增加唯一约束、归属变更版本和下游重算任务，禁止直接更新已结记录。
- **后端：** 对 `(contract_item_id, performance_role)` 建立唯一主归属，修正通过变更单生成差异并触发业绩/工资重算。
- **前端：** 异常与对账页显示重复归属，修正页展示下游影响和差异批次。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:001-quotation-orders`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/performance-allocation-conflicts` | `view` | contractId,status,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/performance-allocation-conflicts/{id}/corrections` | `create` | targetEmployeeId,reason,idempotencyKey | correctionId,approvalId,impactPreview |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/performance-allocation-corrections/{id}/execute` | `execute` | expectedVersion,approvalId,idempotencyKey | performanceAdjustmentId,payrollAdjustmentId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：重复三人提成夹具被唯一约束/冲突扫描识别，修正后业绩和工资差异使用同一 correctionId。
- **前置依赖：** CRM、COURSE、DISC。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 12.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-contracts): close CON-01,CON-02,CON-03,CON-04,CON-05,CON-06,CON-07,CON-08`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 13: Stage 4 · 线上商城（MALL-01、MALL-02、MALL-03）

**依赖：** DISC、CON、PERF。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V101__xuebang_tuition_account_and_mall_quote.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 13.1：逐项差异确认与复用决策

1. 对本域 3 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 13.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V101__xuebang_tuition_account_and_mall_quote.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 13.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 13.4：逐项接口级方案

##### MALL-01 · 不支持多科连报优惠，只支持单科直减/折扣

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 优惠类型已有 `COMBO/MULTI_SUBJECT`，但当前合同订单仍是一单一课程，优惠引擎未校验课程组合是否真正满足联报条件。
- **闭环目标：** 合同草稿改为 `courseItems[]`，组合价格和联报规则按科目数、学段、季节、校区自动匹配并输出逐课程分摊。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 员工制单端接口、管理端 PC、后端 API、PostgreSQL。
- **页面：** `marketing.page-08`（线上商城营销）、`contract.page-01`（价格与套餐）、`contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V101__xuebang_tuition_account_and_mall_quote.sql。领域数据方案：扩展报价快照和合同草稿，新增 `contract_cart_item`，每项保存 courseId、quantity、originalAmount、discountAllocation。
- **后端：** 把商城/员工制单请求改为 courseItems[]，组合报价按科目数、学段、季节和校区匹配联报规则并逐课程分摊。
- **前端：** 线上商城营销和制单审计页提供多科购物车、命中优惠解释及逐课程金额确认。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:038-payment`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | customerId,campusId,courseItems[{courseId,pricePlanId,quantity}],promotionContext,discountRuleIds[] | quoteId,courseItems[],matchedRules[],totalOriginal,totalDiscount,totalPayable |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/carts` | `create` | customerId,campusId,courseItems[],idempotencyKey | cartId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/carts/{id}/confirm-quote` | `execute` | expectedVersion,quoteId | cartVersion,confirmedQuoteSnapshotId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两科/三科组合、跨学段、规则冲突和逐课程分摊合计均有固定金额断言。
- **前置依赖：** DISC、CON、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### MALL-02 · 单科续费也需剩余学费结转审批后形成账户余额再报名

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 售后支持转课并生成财务“重算差额”待办，但没有学费账户余额、结转入账和报名抵扣闭环。
- **闭环目标：** 建立学费账户、结转单、余额流水、冻结/解冻、审批和抵扣顺序；合同支付支持“余额+实付”。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 员工制单端接口、管理端 PC、后端 API、审批中心、PostgreSQL。
- **页面：** `marketing.page-08`（线上商城营销）、`contract.page-08`（合同变更与退转）、`finance.page-05`（收银与资金流水）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V101__xuebang_tuition_account_and_mall_quote.sql。领域数据方案：新增 `tuition_account`、`tuition_transfer_order`、`tuition_balance_ledger`、`tuition_balance_lock`；流水余额只能由事务服务变更。
- **后端：** 建立学费账户、结转单、余额流水、冻结/解冻和审批，合同支付支持余额加实付并固定抵扣顺序。
- **前端：** 合同变更与退转页增加结转试算/审批；制单页展示可用、冻结余额和抵扣明细。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:038-payment`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/tuition-transfers/preview` | `view` | studentId,sourceEntitlementIds[],targetCourseItems[] | transferableAmount,deductions[],warnings[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/tuition-transfers` | `create` | studentId,sourceEntitlementIds[],targetCourseItems[],reason,idempotencyKey | transferId,approvalId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/mixed-payments` | `create` | tuitionBalanceAmount,cashPayments[],expectedVersion,idempotencyKey | paymentId,balanceLedgerId,paidAmount |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：单科续费结转审批后形成余额；并发支付不会超扣；驳回/撤销释放冻结余额。
- **前置依赖：** DISC、CON、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### MALL-03 · 多科报名使用结转+实交时业绩归属可能错；线上合同不能备注具体业绩归属人

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** PRD 提到 performance allocation，当前可执行合同服务只有订单级单一负责人，没有逐课程、逐资金来源业绩归属。
- **闭环目标：** 新增合同课程项与 `PerformanceAllocation`，每课程绑定业绩人、金额来源和比例，合计必须等于实交业绩口径并可审批修正。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 员工制单端接口、管理端 PC、后端 API、业绩引擎、PostgreSQL。
- **页面：** `marketing.page-08`（线上商城营销）、`contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V101__xuebang_tuition_account_and_mall_quote.sql。领域数据方案：复用 `contract_order_item` 并新增 `performance_allocation`，保存 sourceType、employeeId、role、amount、ratio、approvalId。
- **后端：** 合同课程项分别保存资金来源和业绩分配，每课程默认一个主业绩人，比例/金额合计必须匹配认可实交口径。
- **前端：** 制单页逐课程选择业绩人和资金来源；不平衡时阻断提交并显示差额。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:038-payment`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/contract-admin/orders/{id}/performance-allocations` | `edit` | expectedVersion,items[{contractItemId,fundingSources[],allocations[]}] | allocationVersion,totalRecognizedAmount,difference |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/performance-allocations/validate` | `view` | items[] | valid,errors[],recognizedCashByItem |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders/{id}/performance-allocations/change-requests` | `create` | changes[],reason,idempotencyKey | changeRequestId,approvalId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：结转+实交、多课程、多业绩人算例中仅认可现金进入业绩，逐项与总计差额必须为 0。
- **前置依赖：** DISC、CON、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 13.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-mall): close MALL-01,MALL-02,MALL-03`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 14: Stage 5 · 物品管理（ASSET-01、ASSET-02）

**依赖：** PERM。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V102__xuebang_asset_export_tasks.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/assetAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 14.1：逐项差异确认与复用决策

1. 对本域 2 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 14.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V102__xuebang_asset_export_tasks.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 14.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 14.4：逐项接口级方案

##### ASSET-01 · 增加物品导出

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 物料资产有专用页面和完整库存服务，但专用 Controller/View 没有物品导出；通用导出只导 `admin_generic_record`，不能替代资产事实表导出。
- **闭环目标：** 为物品、库存、流水、采购、调拨分别提供异步导出，字段含 SKU ID、仓库 ID、批次、数量、成本和状态。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、导出中心、PostgreSQL。
- **页面：** `asset.page-02`（物料与商品档案）、`asset.page-04`（仓库与库存）、`asset.page-05`（采购管理）、`asset.page-06`（入出库与调拨）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V102__xuebang_asset_export_tasks.sql。领域数据方案：扩展 `sys_import_export_task` 的 resourceKey/filterSnapshot/columnSchema/resultFileId；保存 SKU/仓库/批次稳定 ID。
- **后端：** 为物品、库存、流水、采购、调拨提供资源专用异步导出，查询和导出复用相同权限过滤器。
- **前端：** 各资产页面增加导出列选择、预计行数、任务进度和下载。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/asset/AssetAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/assetAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ast:001-assets`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：resourceType,filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 复用 | 主分支已有，直接复用 | `GET` | `/api/v1/admin/{moduleBase}/{resourceCode}/import-export-tasks/{taskId}` | `view` | path: moduleBase/resourceCode/taskId | 通用导出任务状态、进度、行数、结果文件和错误信息 |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/import-export-files/{fileId}/sign` | `export` | path: moduleBase/resourceCode/fileId | 短期签名下载地址和文件元数据 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：五类导出字段、总行数、数量和成本与页面同条件汇总一致，越权仓库数据不进入文件。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ASSET-02 · 物品类型栏缺滚动条，左右页面应分割

- **补充 PRD 基线：** 未明确；⚪ 待业务验收。
- **最新主分支证据：** 新平台已改为 Ant Design 选择器和分区卡片布局，但静态代码不能证明原数据量下滚动和左右区域是否满足。
- **闭环目标：** 导入真实物品类型后做 1366/1440/1920 视口 UAT；下拉设置最大高度、虚拟滚动，主从区域独立滚动。
- **实施模式：** `UAT_FIRST`。
- **改动端：** 管理端 PC。
- **页面：** `asset.page-02`（物料与商品档案）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：默认不迁移、不建表；仅当执行计划证明缺索引时，才为现有名称/编码/状态查询增加必要索引。
- **后端：** 先使用现有物品类型查询完成真实数据量 UAT；只有 UAT 证明接口未分页或响应过慢时才优化现有查询，不增加解析接口。
- **前端：** 先在 1366×768、1440×900、1920×1080 三个视口验证现有布局；失败后仅修改左侧最大高度、虚拟滚动和左右独立滚动。
- **候选落地文件：** 后端 无默认改动；前端 无默认改动；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ast:001-assets`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

- **接口级决策：** UAT 前无接口改动；页面验收失败且证据指向后端查询时才修改现有物品类型列表接口。

- **最小验证：** 先导入真实类别数据做三视口 UAT；布局失败后补一条聚焦滚动区域的 Playwright，用例通过后不再增加 JUnit/Vitest。
- **前置依赖：** PERM。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 14.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-assets): close ASSET-01,ASSET-02`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 15: Stage 5 · 教师课时费（PAY-01、PAY-02、PAY-03、PAY-04、PAY-05、PAY-06、PAY-07、PAY-08、PAY-09、PAY-10、PAY-11）

**依赖：** COURSE、APPROVAL、ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V103__xuebang_teacher_compensation_engine.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 15.1：逐项差异确认与复用决策

1. 对本域 11 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 15.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V103__xuebang_teacher_compensation_engine.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 15.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 15.4：逐项接口级方案

##### PAY-01 · 班课课时费按教师 T 级、校区班型和当次扣费人数计算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 课时费记录只有教师、班级、课次、金额和状态；没有从 T 级、班型、扣费人数匹配规则并计算金额的执行器。
- **闭环目标：** 建立版本化班课课酬规则矩阵；课次结算后读取实际扣费人数、教师当日 T 级和班型，自动生成计算明细。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端只读接口、后端 API、教务/HR/财务。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-17`（薪资核算与审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：新增 `hr_class_fee_rule_version`、`hr_class_fee_rule_condition`、`hr_class_fee_calculation`、`hr_class_fee_line`，来源唯一键为 lessonId+teacherId+settlementVersion。
- **后端：** 建立班课课酬规则矩阵，课次结算后读取实际成功扣费人数、授课日 T 级、校区班型并自动生成明细。
- **前端：** 课酬页提供规则配置、课次试算、自动核算和逐人扣费证据；工资条只读取已确认课酬。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-rules/simulate` | `view` | teacherId,lessonId,occurredAt | matchedRuleVersion,teacherLevel,deductedStudentCount,unitPrice,amount,explanations[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-settlements` | `create` | lessonIds[],settlementPeriod,idempotencyKey | settlementId,status,lineCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/class-fee-settlements/{id}/lines` | `view` | teacherId?,lessonId?,page,pageSize | records,total,summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：班课固定课次分别用 0/1/满班扣费人数验证规则，冲正学员不计，金额逐行可重放。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-02 · 个性化课时费按 T 级、年级、班型和实际课时计算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 个性化/1v1/1vN 仍共用人工金额记录，未按年级、班型、课时计算。
- **闭环目标：** 建立个性化规则矩阵，输入教师、业务线、学段/年级、班型、课时和特殊系数，输出单价、数量、金额及命中规则。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端只读接口、后端 API、HR/教务。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：规则条件增加 businessLineId、gradeId、schoolStage、classType 和 coefficient；计算行保存实际课时。
- **后端：** 建立个性化课酬矩阵，输入教师、业务线、年级/学段、班型、实际课时和特殊系数计算金额。
- **前端：** 课酬规则页提供个性化模拟器；课酬明细展示单价、数量、系数和命中规则。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/personalized-class-fees/preview` | `view` | teacherId,lessonId,businessLineId,gradeId,classType,actualHours,coefficient | ruleVersion,unitPrice,quantity,coefficient,amount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/personalized-class-fees/calculate` | `view` | lessonIds[],settlementPeriod,idempotencyKey | calculationId,lines[],summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不同年级、1v1/1vN、特殊系数和小数课时算例按精度规则计算并与汇总一致。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-03 · 班课与个性化教师 T 级必须分别维护

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前教师档案没有“教师+业务线+学段+生效期”的 T 级历史，同一岗位信息无法表达两套级别。
- **闭环目标：** 增加教师业务线 T 级档案，同一教师可同时拥有班课 T 级和个性化 T 级；按授课发生日取有效版本。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR。
- **页面：** `hr.page-10`（教师资质）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：使用 `hr_teacher_level_assignment`，批量回填旧单值 T 级为明确业务线的待确认记录。
- **后端：** 同一教师分别维护班课与个性化 T 级，解析服务按业务线、学段和授课发生日返回唯一版本。
- **前端：** 教师资质页将 T 级改为多记录时间线，课酬页显示解析证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:018-teacher-subject`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `GET` | `/api/v1/hr-admin/teacher-level-assignments` | `view` | teacherId,businessLineId,schoolStage,asOf | records,total |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `POST` | `/api/v1/hr-admin/teacher-level-assignments` | `create` | teacherId,businessLineId,schoolStage,levelCode,effectiveFrom,effectiveTo | assignmentId,version |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；teacherId,businessLineId,schoolStage,occurredAt | assignmentId,levelCode |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一教师两套 T 级同时存在且互不覆盖，历史课次解析发生日版本。
- **前置依赖：** PERM-04。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-04 · 当前系统同一老师只能设一个 T 级，无法支撑两套课酬

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 新平台虽然没有继续强制单一 T 级，但也没有实现可执行的双 T 级模型；问题并未因字段缺失而解决。
- **闭环目标：** 禁止在员工主表直接放单值 T 级，改用多条有生效期的等级关系，并增加冲突校验和批量导入。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、迁移工具、HR。
- **页面：** `hr.page-10`（教师资质）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：旧字段只保留兼容读窗口；新增导入批次、行级校验和歧义复核表。
- **后端：** 移除员工主表单值 T 级业务写入，改用有生效期的多条等级关系，并提供批量导入和冲突预检。
- **前端：** 教师 T 级页提供模板下载、预检、错误修正和提交，冲突行不得部分静默覆盖。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:018-teacher-subject`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/teacher-level-imports/preview` | `view` | fileId,businessLineId,defaultEffectiveFrom | importId,validCount,errorCount,rows[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/teacher-level-imports/{id}/execute` | `execute` | idempotencyKey | jobId,createdCount,rejectedCount |
| 修改 | 主分支已有，仅按缺口扩展 | `PUT` | `/api/v1/hr-admin/employees/{id}` | `edit` | 请求中禁止 teacherLevel | employee；出现 teacherLevel 返回 400 |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：导入重复、重叠生效期、未知教师/业务线和合法多 T 级记录均有行级结果。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-05 · 枣庄校区存在单独的课时费计算政策

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 未发现枣庄校区专属规则、规则继承或校区覆盖机制。
- **闭环目标：** 规则支持“集团默认→区域→校区”逐级覆盖，记录覆盖原因、审批人、生效期并提供历史版本回放。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、HR。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：规则版本增加 scopeType/scopeId/parentRuleVersionId/approvalId，禁止同范围同条件生效期重叠。
- **后端：** 课酬规则支持集团默认→区域→校区逐级覆盖，优先级确定且每次覆盖记录原因、审批人和生效期。
- **前端：** 规则矩阵展示继承链和枣庄覆盖差异，可按历史日期回放。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-rules/{id}/overrides` | `create` | scopeType,scopeId,changes,effectiveFrom,effectiveTo,reason,idempotencyKey | overrideId,approvalId |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；campusId,regionId,conditions,occurredAt | resolvedRuleVersion,inheritedFrom,overrideChain[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/class-fee-rules/{id}/versions` | `view` | asOf? | versions[],overrideTree |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：枣庄命中校区规则，其他校区命中区域/集团规则；历史日期解析旧版本。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-06 · 兼职教师有独立课酬标准

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 员工档案虽有用工相关字段，但课时费计算没有读取授课日全职/兼职身份。
- **闭环目标：** 在规则条件中加入 employment type，并按课次日期匹配用工身份历史，不能按当前身份倒算历史。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：计算明细保存 employmentTypeHistoryId 和 resolvedAt。
- **后端：** 课酬规则条件纳入 employmentType，并按课次日期查询用工身份历史，禁止按当前身份倒算。
- **前端：** 规则模拟和课酬明细显示全职/兼职匹配证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-rules/simulate` | `view` | teacherId,lessonId,occurredAt | employmentType,employmentHistoryId,matchedRuleVersion,amount |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/lookups/{entityType}/resolve` | `view` | entityType 按资源注册；employeeId,occurredAt | employmentType,historyId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：兼职转全职前后课次使用各自标准，修改当前身份不改变历史计算。
- **前置依赖：** APPROVAL-03。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-07 · 书法、迪聪、美术等课程按实际扣费人头计算

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 现有规则未区分这些课程，也没有把出勤/扣费明细的人头转为课酬数量。
- **闭环目标：** 为课程经营分类配置计量方式 `DEDUCTED_STUDENT_COUNT`，只统计成功扣费且未冲正的学员。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教务/HR。
- **页面：** `academic.page-02`（课程产品）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：经营分类增加 compensationMeasureType；课次结算保存参与扣费事实 ID 集合。
- **后端：** 书法、迪聪、美术等经营分类配置计量方式 DEDUCTED_STUDENT_COUNT，只统计成功扣费且未冲正学员。
- **前端：** 课程分类页配置计量方式，课酬明细可展开实际扣费学员。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:032-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/academic-admin/course-business-categories/{id}` | `edit` | expectedVersion,compensationMeasureType | categoryVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/class-fee-settlements/{id}/deducted-students` | `view` | lessonId,teacherId,page,pageSize | records,total,validCount,reversedCount |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：成功、失败、冲正和重复扣费事实中只计唯一成功未冲正学员。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-08 · 活动课按活动方案或特殊标准计算课时费

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 营销活动和 HR 课时费是相邻模块，未发现活动方案向课酬传递规则版本。
- **闭环目标：** 活动课程绑定课酬方案；课次结算时把活动 ID、方案 ID、教师角色和核定数量写入课酬来源快照。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、营销/教务/HR。
- **页面：** `marketing.page-03`（活动与公开课）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：新增 `hr_activity_compensation_plan` 和活动关联版本，计算行保存 activityId/planVersion/teacherRole。
- **后端：** 活动课程绑定课酬方案，结算快照记录活动 ID、方案 ID、教师角色和核定数量。
- **前端：** 活动管理选择课酬方案，课酬页面可按活动下钻和复核核定数量。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:002-campaign-budget`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/activity-compensation-plans` | `create` | name,conditions,teacherRoleRates[],effectiveFrom | planId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/activity-compensation-plans/{id}/bind` | `create` | activityId,effectiveFrom,idempotencyKey | bindingId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/activity-class-fees/calculate` | `view` | activityId,lessonIds[],idempotencyKey | calculationId,lines[],summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一活动主讲/助教不同标准、活动取消和核定数量调整均有明确结果。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-09 · 跨月补录、系统锁定后调整会导致课时费漏算或错月

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 薪资批次按课时费记录 `created_at` 汇总，无法保证按授课发生月归属；也没有关账后追补机制。
- **闭环目标：** 改按 `lessonOccurredAt/payrollPeriod` 归属；关账后新增差异单进入下一期追补，不直接篡改已结薪资批次。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端接口、后端 API、HR/工资。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-17`（薪资核算与审批）、`hr.page-18`（电子工资条与异议）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：新增 `hr_class_fee_adjustment`，保存 originalPeriod、targetPayrollPeriod、reason、sourceChangeId 和正负差额。
- **后端：** 按 lessonOccurredAt 确定课酬期间；关账后补录生成下一期追补差异，不修改已结工资批次。
- **前端：** 课酬页增加跨月差异队列，工资核算页展示本期追补来源。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-adjustments/preview` | `view` | lessonId,changeType,changedValues,occurredAt | originalAmount,recalculatedAmount,difference,originalPeriod,targetPeriod |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-adjustments` | `create` | previewId,reason,idempotencyKey | adjustmentId,status,targetPayrollPeriod |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/payroll-batches/{id}/class-fee-adjustments` | `view` | page,pageSize | records,total,summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨月补录和锁定后调整进入下一期正负追补，已发布工资条金额不被覆盖。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-10 · 常规课程需按统一常规课标准计算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 目前所有课时费均可人工填写，未发现常规课程默认规则和例外审批。
- **闭环目标：** 建立集团常规课默认规则；未命中规则时阻断结算并进入异常池，禁止默认为零或手填绕过。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR 异常中心。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-21`（人力异常治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：新增规则覆盖率扫描和 `hr_compensation_exception`，异常保存未命中条件和责任人。
- **后端：** 建立集团常规课默认规则；任何课次未命中规则时阻断结算并生成异常，禁止默认为零或手工金额。
- **前端：** 规则页显示覆盖率，课酬异常页支持补规则后重算。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fee-rules/coverage-check` | `create` | periodFrom,periodTo,campusIds[] | coveredCount,uncoveredCount,uncoveredCombinations[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/compensation-exceptions` | `view` | status,errorCode,period,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/compensation-exceptions/{id}/retry` | `execute` | expectedVersion,idempotencyKey | status,calculationLineId? |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：常规课命中默认规则；删除覆盖规则后结算失败并进入异常池，补规则重试成功。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PAY-11 · 小学与中学混合/不同学段的班级课时费口径不同

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 课程/班级有年级信息，但课时费记录和规则没有学段条件，也未定义混合班取值。
- **闭环目标：** 规则加入学段；混合班明确按课程主学段、实际扣费学员学段加权或专项规则计算，并由业务选定唯一口径。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教务/HR。
- **页面：** `academic.page-04`（班级与学员）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V103__xuebang_teacher_compensation_engine.sql。领域数据方案：班级增加 compensationStagePolicy 和审批版本，计算明细保存每学段人数/权重。
- **后端：** 规则加入学段；混合班只允许选择一种已审批口径：主学段、实际扣费学员学段加权或专项规则。
- **前端：** 班级详情配置混合班口径，课酬明细展示学段贡献。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/TeacherCompensationService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/classes/{id}/compensation-stage-policies` | `create` | mode,primarySchoolStage?,weights?,specialRuleId?,reason | policyId,approvalId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/class-fees/mixed-stage-preview` | `view` | classId,lessonId,policyId | stageBreakdown[],unitPrices[],amount |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：小学、中学和混合班三种场景按唯一已审批口径计算，未配置混合口径阻断结算。
- **前置依赖：** COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 15.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-teacher-pay): close PAY-01,PAY-02,PAY-03,PAY-04,PAY-05,PAY-06,PAY-07,PAY-08,PAY-09,PAY-10,PAY-11`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 16: Stage 5 · 业绩核算（PERF-01、PERF-02、PERF-03、PERF-04、PERF-05、PERF-06）

**依赖：** CRM、CON、COURSE、APPROVAL、ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V104__xuebang_performance_settlement_engine.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 16.1：逐项差异确认与复用决策

1. 对本域 6 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 16.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V104__xuebang_performance_settlement_engine.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 16.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 16.4：逐项接口级方案

##### PERF-01 · 不同区域、岗位采用不同业绩标准

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** HR 有通用绩效规则和组织/岗位数据，但规则只是配置载体，没有读取合同/实收并执行区域岗位公式。
- **闭环目标：** 建立业绩规则 DSL/结构化 Schema、版本和计算服务，以业务发生日匹配区域、校区、岗位和员工任职。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、业绩结算、PostgreSQL。
- **页面：** `hr.page-12`（绩效目标）、`hr.page-15`（课酬与佣金）、`bi-dashboard.page-04`（销售与顾问绩效）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `performance_rule`、`performance_rule_version`、条件/动作行和 `performance_calculation_evidence`，禁止重叠优先级产生两个有效结果。
- **后端：** 建立结构化业绩规则版本，按业务发生日匹配区域、校区、岗位、任职关系和业务线，计算结果保存完整规则证据。
- **前端：** 绩效规则页提供矩阵编辑、冲突校验和单笔模拟；结算详情显示命中区域岗位版本。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:007-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/rules` | `create` | name,metricType,conditions[],action,priority,effectiveFrom,effectiveTo | ruleId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/rules/validate` | `view` | conditions[],action,priority,effectivePeriod | valid,errors[],conflicts[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/rules/simulate` | `view` | employeeId,positionId,businessEventId,occurredAt | matchedRuleVersionId,value,evidence[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一员工跨区域调岗前后业务按发生日匹配不同标准；冲突规则不能发布，历史结算不随当前岗位变化。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERF-02 · 人头应去重，扩科/多科的人次与科次需另算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前没有统一的人头、人次、科次事件口径，也没有在业绩结算中按稳定学员 ID 去重。
- **闭环目标：** 定义 `student_headcount/course_signup_count/subject_count` 三类指标，明确周期和去重键，并保存逐笔贡献明细。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、BI/业绩结算。
- **页面：** `hr.page-12`（绩效目标）、`bi-dashboard.page-04`（销售与顾问绩效）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `performance_metric_contribution`，保存 metricType、dedupKey、period、sourceEventId、studentId、courseId、subjectId。
- **后端：** 固化学员人头、报名人次和科次三类指标，分别使用稳定学员 ID、合同课程项和科目 ID 作为周期去重键。
- **前端：** 结算页同时展示三类数值及逐笔贡献，下钻标识被去重记录。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:007-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/performance-admin/metric-definitions` | `view` | metricTypes[],version? | definitions[{formula,dedupKey,periodSemantics,exclusions}] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/metrics/calculate` | `view` | metricTypes[],period,employeeIds[],scope,idempotencyKey | calculationId,totalsByMetric |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/performance-admin/calculations/{id}/contributions` | `view` | metricType,employeeId,dedupStatus,page,pageSize | records,total,summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员扩两科时人头 1、人次按约定事件、科次 2；跨周期去重重新开始，逐笔合计等于汇总。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERF-03 · 业绩按实际现金计算：定金如何计入、结转余额不计实交

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** BI 有净现金指标，但合同业绩未区分现金、定金、账户余额、结转和退款资金来源。
- **闭环目标：** 支付流水增加资金来源；业绩只聚合规则认可的实收，定金在收取或转正式时按统一政策归属，结转单列不重复计算。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、合同/收款/业绩。
- **页面：** `finance.page-05`（收银与资金流水）、`contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `payment_funding_allocation`、`performance_funding_policy_version`，合同项保存现金认可快照和来源流水 ID。
- **后端：** 为支付流水标注 CASH/DEPOSIT/TUITION_BALANCE/TRANSFER/REFUND 等资金来源，业绩只读取规则认可现金，定金归属采用统一版本政策。
- **前端：** 收银和业绩详情展示资金来源、认可/排除金额及定金转正式链路。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `fin:001-cashier-desk`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/performance-admin/funding-policies/{id}/versions` | `view` | version? | recognizedSourceTypes[],depositRecognitionPoint,effectiveFrom |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/funding-recognition/preview` | `view` | contractId,paymentIds[],asOf | allocations[],recognizedCash,excludedAmount,evidence[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/deposits/{paymentId}/formalize` | `create` | contractItemAllocations[],policyVersionId,idempotencyKey | recognizedEvents[],recognizedAmount |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：现金、定金、余额、结转和退款组合中只认可规定来源；定金只在政策指定时点记一次，退款形成负向贡献。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERF-04 · 多课程、多业绩人时应按课程逐项归属

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 当前合同仍是一订单一课程一负责人，不能表达多课程逐项业绩归属。
- **闭环目标：** 合同课程项绑定主业绩人和可选协作人；逐项金额与实收分摊校验后才允许盖章。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工制单端接口、后端 API、合同/业绩。
- **页面：** `contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `performance_allocation` 和版本表，唯一约束每合同项一个 PRIMARY，分配行合计与 recognizedCash 快照相等。
- **后端：** 合同每个课程项绑定一个主业绩人和可选协作人，按认可实收逐项分配，金额/比例不平衡时阻断盖章。
- **前端：** 制单页逐课程配置业绩人和协作比例，实时显示差额；盖章详情锁定分配版本。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/contract-admin/orders/{id}/performance-allocations` | `edit` | expectedVersion,items[{contractItemId,primaryEmployeeId,collaborators[{employeeId,ratio}],amounts[]}] | allocationVersion,differences[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/allocations/validate` | `view` | contractId,allocations[] | valid,recognizedCashByItem,differences[],errors[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/performance-admin/contracts/{contractId}/allocations` | `view` | version? | items[],recognizedCash,totalAllocated,reconciliationDiff |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：三课程、四业绩人组合逐项合计为认可实收；缺主业绩人、比例超 100% 和跨权限员工均被阻断。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERF-05 · 常规课程与活动课程采用不同业绩标准

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 课程/营销活动可区分，但未形成活动方案到业绩计算的可执行映射。
- **闭环目标：** 活动方案绑定业绩规则版本；合同项保存活动 ID，结算时分别输出常规和活动贡献。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、营销/课程/业绩。
- **页面：** `marketing.page-03`（活动与公开课）、`contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `marketing_activity_performance_policy`，活动有效期与规则版本有效期发布时校验。
- **后端：** 活动方案绑定业绩规则版本，合同项保存 activityId 和 activityPerformanceRuleVersionId，常规与活动贡献分开结算。
- **前端：** 活动配置增加业绩规则选择和模拟；业绩明细用贡献类型区分 REGULAR/ACTIVITY。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/marketing/MarketingAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/marketing/MarketingAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/marketing/MarketingAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/marketingAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:002-campaign-budget`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/marketing-admin/activities/{id}/performance-policy` | `edit` | expectedVersion,ruleVersionId,effectiveFrom,effectiveTo | policyId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/activity-contributions/calculate` | `view` | activityId,period,idempotencyKey | calculationId,regularTotal,activityTotal |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/performance-admin/activity-contributions` | `view` | activityIds[],contributionTypes[],period,page,pageSize | records,total,summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同课程在常规和活动两笔合同分别命中对应规则；活动过期后不再命中，汇总可分别对账。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### PERF-06 · 需形成覆盖签单类型、课程、岗位、区域、退款的完整规则矩阵

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 现有规则底座不能证明这些业务条件可执行，退款也只生成待办而非重算结果。
- **闭环目标：** 先固化业务口径，再实现试算、结算、确认、申诉、关账和追补全状态机；用历史月份逐笔回放验收。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端接口、后端 API、审批/工资/BI。
- **页面：** `hr.page-12`（绩效目标）、`hr.page-15`（课酬与佣金）、`hr.page-18`（电子工资条与异议）、`hr.page-21`（人力异常治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V104__xuebang_performance_settlement_engine.sql。领域数据方案：新增 `performance_settlement_run`、`performance_statement`、`performance_appeal`、`performance_revision`、`performance_close`，原版本只追加不覆盖。
- **后端：** 实现试算、结算、员工确认、申诉、修正、关账和退款追补状态机，规则矩阵覆盖签单类型、课程、岗位、区域和退款。
- **前端：** 业绩工作台提供批次、员工账单、规则证据、异议和关账；员工端确认或申诉。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/performance/PerformanceAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/performanceAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:007-settings`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/settlement-runs` | `create` | period,scope,ruleSetVersionId,mode=PREVIEW\|FINAL,idempotencyKey | runId,status,statementCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/statements/{id}/confirmations` | `execute` | decision,comment,evidenceFileIds[],idempotencyKey | confirmationId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/settlement-runs/{id}/close` | `execute` | expectedVersion,approvalInstanceId,idempotencyKey | status,closedAt,ledgerWatermark |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/performance-admin/refund-adjustments` | `create` | refundId,idempotencyKey | adjustmentRunId,affectedStatements[],totalDifference |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：历史月份逐笔回放后明细合计、账单、BI 和工资台账一致；关账后只允许差异追补，重复退款不重复冲减。
- **前置依赖：** CRM、CON、COURSE、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 16.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-performance): close PERF-01,PERF-02,PERF-03,PERF-04,PERF-05,PERF-06`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 17: Stage 5 · 过渡奖励（TRANS-01、TRANS-02、TRANS-03、TRANS-04、TRANS-05、TRANS-06、TRANS-07、TRANS-08、TRANS-09、TRANS-10、TRANS-11、TRANS-12、TRANS-13、TRANS-14、TRANS-15）

**依赖：** CON、PERF、APPROVAL、ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V105__xuebang_transition_reward_engine.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 17.1：逐项差异确认与复用决策

1. 对本域 15 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 17.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V105__xuebang_transition_reward_engine.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 17.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 17.4：逐项接口级方案

##### TRANS-01 · 不同校区/学段/课程的过渡奖励标准不同，当前需人工判断

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 未发现过渡奖励规则表或计算服务。
- **闭环目标：** 建立奖励规则矩阵，条件至少包括校区、业务线、学段、课程/产品线、签单类型、岗位和生效期。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR/财务。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-16`（薪资规则与工资档案）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增 `transition_reward_rule`、`transition_reward_rule_version`、结构化条件/动作表。
- **后端：** 建立按校区、业务线、学段、课程/产品线、签单类型、岗位和生效期匹配的奖励规则矩阵。
- **前端：** 奖励规则页提供矩阵、冲突检测和模拟器。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rules` | `create` | name,conditions,baseType,action,effectiveFrom,effectiveTo | ruleId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rules/simulate` | `view` | ruleVersionId,studentId,contractItemId,occurredAt | matched,base,amount,explanations[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rules/{id}/publish` | `execute` | expectedVersion | publishedVersion,conflicts[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不同校区/学段/课程固定算例命中唯一规则，重叠规则发布被阻断。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-02 · 历史奖励记录分散，难以追溯某学员曾给谁、给多少

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 学员、合同、订单和审批有审计链，但没有专门奖励台账把它们串成一笔奖励。
- **闭环目标：** 新增奖励主表和明细表，保存学员、合同、课程、奖励人、金额、规则版本、状态、来源事件和调整链。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、教师员工端只读接口、后端 API、PostgreSQL。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-18`（电子工资条与异议）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增 `transition_reward`、`transition_reward_line`、`transition_reward_adjustment`。
- **后端：** 建立奖励主表和明细，保存学员、合同、课程、奖励人、金额、规则版本、状态、来源事件和调整链。
- **前端：** 奖励台账按学员/员工/合同查询，详情展示完整来源和调整链。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/rewards` | `view` | studentId,employeeId,contractId,statuses[],period,page,pageSize | records,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/rewards/{id}` | `view` | path: id | reward,lines[],sourceEvents[],adjustments[] |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员多合同多奖励人记录可按稳定 ID 查询，明细合计等于主表金额。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-03 · 奖励金额需跨客户、合同、课消、退款等模块核对

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 各模块已有稳定 ID 和部分事件/待办，但没有统一奖励聚合与自动重算。
- **闭环目标：** 以领域事件驱动奖励试算，所有输入保存 ID 和数值快照；支持按合同或学员重放计算。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、事件总线、PostgreSQL。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增奖励输入快照和 sourceEventId 唯一键，消费使用 runtime outbox 幂等。
- **后端：** 奖励试算由客户、合同、课消、退款领域事件驱动，所有输入保存 ID 和数值快照并支持重放。
- **前端：** 奖励详情提供“重放计算”并显示原值/新值差异。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/calculations` | `create` | sourceEventId,eventType,studentId,occurredAt,idempotencyKey | calculationId,candidateRewardIds[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/calculations/{id}/replay` | `create` | ruleVersionId?,reason,idempotencyKey | replayId,differences[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/calculations/{id}/inputs` | `view` | path: id | inputSnapshot,sourceIds[],ruleVersion |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：重复事件不重复奖励；相同快照和规则版本重放结果字节级一致。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-04 · 新签、续费、扩科、转介绍、召回等签单类型的奖励规则不同

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 现有合同没有完整、自动判定且版本化的签单类型规则。
- **闭环目标：** 先建立签单类型判定服务，再由奖励规则引用；人工改类必须审批并保留原判定。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、合同/奖励。
- **页面：** `contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：奖励行保存 signingType、evidenceId 和 signingTypeRuleVersion。
- **后端：** 奖励计算先调用签单类型判定服务，新签、续费、扩科、转介绍、召回分别匹配规则；人工改类必须审批。
- **前端：** 奖励详情展示签单类型证据及纠错入口。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `POST` | `/api/v1/contract-admin/signing-types/evaluate` | `view` | studentId,courseItems[],signedAt | signingType,ruleVersion,evidenceEvents[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rewards/{id}/signing-type-change-requests` | `create` | requestedType,reason,evidence,idempotencyKey | requestId,approvalId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五类签单类型各一套奖励算例，未经审批的人工改类不影响奖励。
- **前置依赖：** CON-06、TRANS-15。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-05 · 奖励基数、人数/金额口径及签单类型判断不统一

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 没有统一“奖励基数”字段、公式版本或口径字典。
- **闭环目标：** 规则显式声明基数是实收、净收、合同额、人头或人次，注明定金、余额、结转、退款和优惠处理方式。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、财务。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：规则动作增加 baseType 和 fundingSourcePolicies；奖励行保存 recognizedBase 和 excludedAmounts。
- **后端：** 奖励规则显式声明基数类型和定金、余额、结转、退款、优惠处理方式，计算明细逐资金来源解释。
- **前端：** 规则模拟器展示每种资金来源是否计入及原因。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/reward-bases/preview` | `view` | contractId,ruleVersionId,asOf | fundingSources[],recognizedBase,excludedAmount,reasons[] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rules` | `create` | baseType,fundingSourcePolicies,refundPolicy,discountPolicy | ruleId,version |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：实收、净收、合同额、人头、人次及定金/结转/退款组合均有固定基数断言。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-06 · 需要在统一学员视图查看过渡奖励

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 新平台已有稳定学员主档和跨模块关系，具备承载条件；当前学员详情未呈现奖励。
- **闭环目标：** 在学员 360 增加奖励页签，展示资格、试算、冻结、确认、发放、追回及关联合同/退款。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、Student 360。
- **页面：** `crm.page-03`（客户档案）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：使用奖励台账读模型，不在 CRM 复制金额事实。
- **后端：** 在学员 360 增加奖励页签，展示资格、试算、冻结、确认、发放、追回及关联合同/退款。
- **前端：** 学员详情按状态分组显示奖励，支持跳转合同、退款、工资差异。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/students/{studentId}/rewards` | `view` | statuses[],page,pageSize | records,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/students/{studentId}/reward-summary` | `view` | asOf | eligible,frozen,confirmed,paid,clawedBack |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：Student 360 汇总等于奖励台账同条件合计，字段权限隐藏其他员工敏感薪酬。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-07 · 系统自动给符合条件的学员打标签并生成奖励基数

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 未发现奖励资格识别任务、自动标签或基数生成器。
- **闭环目标：** 奖励事件处理器命中规则后生成系统标签和候选奖励，业务标签与人工标签分层，禁止人工覆盖系统事实。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、事件总线、CRM。
- **页面：** `crm.page-03`（客户档案）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增 `stu_system_tag`、`transition_reward_candidate`，标签来源唯一关联 calculationId。
- **后端：** 规则命中后生成系统标签和候选奖励，系统标签与人工标签分层且不可人工覆盖。
- **前端：** 学员详情区分系统/人工标签；候选奖励队列显示命中证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/candidates/generate` | `create` | sourceEventIds[],idempotencyKey | jobId,candidateCount,tagCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/candidates` | `view` | status,ruleId,campusId,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/students/{id}/system-tags` | `view` | tagTypes[],asOf | items[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：符合条件自动生成一次候选和系统标签；人工编辑系统标签返回 403。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-08 · 退款后需改变续费判断并追溯过渡奖励

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 售后退款会生成绩效/财务 `PENDING` 待办，但不会实际重判签单类型或奖励。
- **闭环目标：** 退款完成事件触发签单类型与奖励重算，生成差额追回单；已发放的进入薪资追补，未发放的直接冲减冻结额。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、退款/工资。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-17`（薪资核算与审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增 reward clawback 主/明细，保存 refundId、originalRewardId、payrollAdjustmentId。
- **后端：** 退款完成事件触发签单类型和奖励重算，未发奖励冲减冻结额，已发奖励生成工资追补。
- **前端：** 奖励详情显示退款影响和追回状态，工资批次显示来源退款。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/refund-recalculations` | `create` | refundId,idempotencyKey | recalculationId,affectedRewards[],totalClawback |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/clawbacks/{id}/execute` | `execute` | expectedVersion,idempotencyKey | status,payrollAdjustmentId? |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/refunds/{refundId}/impact` | `view` | path: refundId | rewards[],clawbacks[],payrollAdjustments[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：冻结中退款直接冲减，已发放退款生成负向工资差异，重复退款事件幂等。
- **前置依赖：** REFUND-07。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-09 · 奖励需冻结 31 天后再确认，期间发生退款要扣除

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 没有奖励冻结期、到期任务和冻结期退款监听。
- **闭环目标：** 奖励状态机设 `CALCULATED→FROZEN→CONFIRMED→PAID/CLAWED_BACK`，冻结截止按规则计算并由定时任务推进。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端接口、后端 API、调度器。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-18`（电子工资条与异议）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：奖励主表增加 frozenUntil、confirmedAt、paidAt 和状态迁移审计。
- **后端：** 奖励状态机固定 CALCULATED→FROZEN→CONFIRMED→PAID/CLAWED_BACK，冻结截止按规则计算，定时任务推进。
- **前端：** 冻结队列展示剩余天数，员工确认端只允许到期后的合法动作。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rewards/{id}/freeze` | `create` | expectedVersion | status,frozenUntil |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/frozen/advance` | `create` | asOf,idempotencyKey | advancedCount,blockedCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rewards/{id}/confirm` | `execute` | expectedVersion,employeeConfirmation,idempotencyKey | status,confirmedAt |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：31 天边界前不能确认，到期可确认，冻结期间退款进入追回；非法状态迁移返回 409。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-10 · 奖励规则需按校区、年级、课程等维度配置

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 当前通用绩效规则 `configJson` 没有过渡奖励专属字段、校验或执行器。
- **闭环目标：** 使用结构化条件和动作 Schema，不用无约束 JSON 直接做结算；提供冲突检测和规则模拟。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL。
- **页面：** `hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：条件拆分到 typed rows，数据库约束 dimensionType/operator/valueType。
- **后端：** 奖励规则使用结构化条件和动作 Schema，提供维度冲突检测和规则模拟，禁止无约束 JSON 直接结算。
- **前端：** 规则编辑器按校区、年级、课程等维度生成表单并展示冲突区间。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rules/validate` | `view` | conditions[],action,effectiveFrom,effectiveTo | valid,errors[],conflicts[] |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `POST` | `/api/v1/transition-reward-admin/rules/simulate` | `view` | ruleVersionId,studentId,contractItemId,occurredAt | matched,amount,explanations[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：类型错误、空范围、冲突规则和合法规则分别断言 400/409/成功。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-11 · 需要独立计算页、明细查询和导出

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 菜单/元数据里没有过渡奖励核算页，通用导出也没有奖励事实表。
- **闭环目标：** 建立候选、冻结、待确认、已发放、异常五类队列及异步明细导出，导出包含全部计算输入。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、导出中心。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-20`（通知与人事报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：奖励状态索引和导出快照保存 filterHash、ruleVersions、rowCount。
- **后端：** 建立候选、冻结、待确认、已发放、异常五类队列及异步明细导出，导出包含全部计算输入。
- **前端：** 课酬与佣金页增加奖励工作台的五个队列和导出任务。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/work-queues/{queueType}` | `view` | filters,page,pageSize | records,total,summary |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：queueType,filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 复用 | 主分支已有，直接复用 | `GET` | `/api/v1/admin/{moduleBase}/{resourceCode}/import-export-tasks/{taskId}` | `view` | path: moduleBase/resourceCode/taskId | 通用导出任务状态、进度、行数、结果文件和错误信息 |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五队列互斥/覆盖规则明确，导出行与查询快照一致且包含来源 ID 和规则版本。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-12 · 合同、课消、退款变化后应自动同步，不应人工搬数

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 相邻模块有服务和部分任务，但没有可靠的奖励事件订阅、幂等和重放机制。
- **闭环目标：** 建立 outbox 事件与幂等键，消费失败进入重试/死信；每日运行对账任务查找漏算。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 后端 API、事件总线、运维端、PostgreSQL。
- **页面：** `system-ops.page-04`（调度与异步任务）、`system-ops.page-05`（MQ 与死信）、`hr.page-21`（人力异常治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：复用 runtime event/outbox，新增奖励事件契约和 reconciliation finding。
- **后端：** 合同、课消、退款变化通过 outbox 推送奖励事件，消费者幂等重试，死信告警，每日对账查漏算。
- **前端：** 运维页面按 reward 事件查看重试/死信；人力异常页显示漏算差异。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ops:002-tasks-center`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/events/reconcile` | `view` | periodFrom,periodTo,eventTypes[],idempotencyKey | jobId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/reconciliation-findings` | `view` | status,errorType,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/reconciliation-findings/{id}/repair` | `execute` | expectedVersion,idempotencyKey | eventId,recalculationId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：模拟消费失败进入重试/死信；对账识别漏事件并修复；同 eventId 只生成一份奖励结果。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-13 · 需要“核算—员工确认—异议—修正—终审”闭环

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 通用审批、任务转交和薪资批次可复用，但奖励的确认、异议和修正对象不存在。
- **闭环目标：** 基于审批底座实现奖励确认单和异议单；修正生成新版本/差异项，不修改已确认原记录。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、教师员工端接口、后端 API、审批中心。
- **页面：** `hr.page-15`（课酬与佣金）、`hr.page-18`（电子工资条与异议）、`hr.page-19`（人事审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：新增 `transition_reward_confirmation`、`transition_reward_objection`、`transition_reward_revision`。
- **后端：** 实现核算→员工确认→异议→修正→终审闭环，修正生成新版本和差异项而非修改原记录。
- **前端：** 员工端确认/异议，管理端处理、修正和终审；详情保留版本对比。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:004-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rewards/{id}/employee-confirmations` | `execute` | decision,comment,idempotencyKey | confirmationId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/rewards/{id}/objections` | `create` | reason,evidenceFileIds[],idempotencyKey | objectionId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/objections/{id}/revisions` | `create` | adjustmentLines[],reason,idempotencyKey | revisionId,approvalId,difference |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/revisions/{id}/final-approve` | `execute` | expectedVersion,decisionComment | status,rewardVersion |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：员工确认和异议互斥，修正保留原金额，终审后只发布差异版本。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-14 · 学员转校、转班、转课等轨迹要参与奖励判定

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 售后变更保留审批和执行轨迹，但没有投影成奖励可查询的统一时间线。
- **闭环目标：** 建立学员业务事件时间线，奖励计算按事件发生日读取校区、课程和归属关系，避免只看当前状态。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、Student 360、事件总线。
- **页面：** `crm.page-03`（客户档案）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：复用 stu_timeline_event，奖励输入快照保存 eventContextVersion。
- **后端：** 奖励按业务事件发生日读取转校、转班、转课前后的校区、课程和归属，不读取当前主档替代历史。
- **前端：** 奖励详情展示参与判定的转校/转班/转课时间线片段。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/transition-reward-admin/students/{id}/eligibility-timeline` | `view` | from,to,eventTypes[] | events[],resolvedContexts[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/transition-reward-admin/eligibility/evaluate` | `view` | studentId,occurredAt,sourceEventId | eligible,context,ruleVersion,evidenceEvents[] |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：转校前后两笔合同分别使用当时校区和课程关系，当前主档变化不改历史结果。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### TRANS-15 · 系统应自动判断合同属于新签、续费、扩科、转介绍或召回

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前没有覆盖全部类型的统一判定服务；CRM 来源、历史权益和合同之间未形成可执行公式。
- **闭环目标：** 按稳定学员 ID 查询历史合同/权益/课消和归因，输出签单类型、证据和规则版本；允许审批纠错。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、员工制单端接口、后端 API、合同/CRM。
- **页面：** `contract.page-05`（员工端制单审计）、`hr.page-15`（课酬与佣金）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V105__xuebang_transition_reward_engine.sql。领域数据方案：签单类型判定保存 ruleVersion、evidenceHash 和人工纠错审批链。
- **后端：** 按稳定学员 ID 查询历史合同、权益、课消和归因，自动输出新签/续费/扩科/转介绍/召回及证据。
- **前端：** 制单页和奖励详情共享同一判定组件与证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/reward/TransitionRewardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/transitionRewardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:020-staff-contract-order-records`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `POST` | `/api/v1/contract-admin/signing-types/evaluate` | `view` | studentId,courseItems[],signedAt,attributionId? | signingType,ruleVersion,evidenceEvents[],confidence |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/signing-types/rules/{id}/decision-table` | `view` | version? | conditions[],precedence,examples[] |
| 复用 | 通用能力需做一次共享扩展，禁止按问题复制 | `POST` | `/api/v1/contract-admin/orders/{id}/signing-type-change-requests` | `create` | requestedType,reason,evidence | requestId,approvalId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五种类型、边界优先级和审批纠错均有固定历史数据回放；合同与奖励读取相同结果。
- **前置依赖：** CON、PERF、APPROVAL、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 17.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-transition-reward): close TRANS-01,TRANS-02,TRANS-03,TRANS-04,TRANS-05,TRANS-06,TRANS-07,TRANS-08,TRANS-09,TRANS-10,TRANS-11,TRANS-12,TRANS-13,TRANS-14,TRANS-15`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 18: Stage 5 · 退费扣款（REFUND-01、REFUND-02、REFUND-03、REFUND-04、REFUND-05、REFUND-06、REFUND-07、REFUND-08）

**依赖：** CON、DISC、PAY、TRANS、PERF。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V106__xuebang_refund_settlement_engine.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 18.1：逐项差异确认与复用决策

1. 对本域 8 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 18.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V106__xuebang_refund_settlement_engine.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 18.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 18.4：逐项接口级方案

##### REFUND-01 · 同一学员多合同退费需逐笔处理，缺少批量核算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 售后退款请求一次只关联一个订单/权益，没有多合同批次和统一试算。
- **闭环目标：** 创建退费批次，选择多份合同/权益后分别试算、统一审批、逐笔执行；一笔失败不得污染其他明细。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、财务/售后台账。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`finance.page-05`（收银与资金流水）、`contract.page-08`（合同变更与退转）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_batch`、`refund_batch_item`、`refund_batch_item_result`；批次状态 DRAFT→PREVIEWED→SUBMITTED→APPROVED→EXECUTING→PARTIAL_SUCCESS/SUCCEEDED/FAILED。
- **后端：** 建立多合同退费批次状态机，先逐合同/权益试算，再统一提交审批，审批通过后逐明细独立事务执行并汇总结果。
- **前端：** 服务工作台增加批量退费向导，按稳定学员 ID 勾选合同和权益，展示逐项试算、统一审批状态与可重试失败项。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/batches/preview` | `view` | studentId,items[{contractId,contractItemId,entitlementId,requestedAmount}],asOf,idempotencyKey | previewToken,items[{refundableAmount,deductions,warnings}],totals |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/batches` | `create` | previewToken,reason,evidenceFileIds[],idempotencyKey | refundBatchId,approvalId,status,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/batches/{id}/execute` | `execute` | expectedVersion,approvedInstanceId,idempotencyKey | status,succeededItems[],failedItems[],totals |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员三份合同夹具中两笔成功、一笔失败时批次为 PARTIAL_SUCCESS；成功流水只生成一次，失败项修复后可幂等重试。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-02 · 财务需在一个页面看学员全部合同、余额、课消、优惠和退款

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 各域可按 ID 查询，但没有退费工作台统一聚合，也没有同口径总额校验。
- **闭环目标：** 建立退费 360 工作台，按稳定学员 ID 聚合合同项、权益余额、课消、出勤、优惠快照、结转和历史退款。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、CRM/合同/教务/财务。
- **页面：** `after-sales.page-01`（服务工作台）、`crm.page-03`（客户档案）、`finance.page-05`（收银与资金流水）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_student_360_projection` 和投影水位，金额分项带 sourceType/sourceId/version，响应附 reconciliationDiff。
- **后端：** 提供退费 360 只读聚合服务，以 canonicalStudentId 同时返回合同、合同项、权益余额、课消、考勤、优惠、学费余额、结转和退款历史。
- **前端：** 服务工作台增加退费 360 抽屉，按业务区块展示金额、来源单据和风险提示；差异不为零时禁止提交退费。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:001-ownership`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/students/{studentId}/refund-360` | `view` | asOf,includeVoided | student,contracts[],entitlements[],consumptions[],attendances[],discounts[],tuitionAccount,transfers[],refunds[],totals,reconciliationDiff |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/students/{studentId}/refund-360/refresh` | `create` | asOf,idempotencyKey | projectionVersion,watermarks[],reconciliationDiff |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/students/{studentId}/refundable-items` | `view` | contractIds[],asOf,page,pageSize | records,total,summary |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨三份合同、含余额结转和部分退款的数据中，各区块来源 ID 可打开，余额恒等式成立且总差额为 0。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-03 · 转校/转课/课消/退款明细需连续可追溯

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 售后变更、权益流水和审计日志已有关系链；当前没有面向财务的完整时间线与导出。
- **闭环目标：** 统一业务事件时间线，所有变更记录前值/后值、来源单据、执行人和发生时间，并支持导出。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、Student 360、导出中心。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`crm.page-03`（客户档案）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：扩展 `stu_timeline_event` 的退款可见性与财务字段，导出快照保存 filterHash、scopeHash、eventWatermark。
- **后端：** 复用统一业务事件时间线，将转校、转班、转课、扣费、冲正和退款按发生时间串联，事件记录前后值、来源单据和操作人。
- **前端：** 退费详情增加财务时间线和事件类型筛选，支持异步导出当前查询快照。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/students/{studentId}/business-timeline` | `view` | eventTypes[],from,to,cursor,pageSize | records,nextCursor,watermark |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/timeline-events/{eventId}` | `view` | path: eventId | event,beforeSnapshot,afterSnapshot,sourceLink,actor,audit |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：studentId,eventTypes[],from,to,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：完成转校→转课→课消→冲正→退款链路，页面和导出顺序、前后值、单据 ID、操作人完全一致。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-04 · 退款后多科优惠应降档并扣回已享优惠

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 退款只校验不超过订单实付，未重放报价快照或重新定档。
- **闭环目标：** 在退款试算中基于退后课程组合重新报价，逐课程分摊优惠差额，并将应扣金额列入退款公式。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、优惠引擎、合同/财务。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`contract.page-02`（优惠规则）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_discount_reprice_snapshot`，保存 originalQuoteSnapshotId、remainingItems、matchedRuleVersions、allocationBefore/After 和 clawbackAmount。
- **后端：** 退费试算先移除拟退课程项，再按原报价时点的规则版本重放剩余组合，逐课程分摊优惠降档差额并计入退款扣减。
- **前端：** 试算页并排展示原优惠、退后优惠、逐课程扣回和净退款；允许查看规则解释但不得手改系统金额。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/quotes/refund-replay` | `create` | contractId,refundItems[{contractItemId,quantity}],refundAt | originalQuote,repricedQuote,discountClawbacks[],netRefundAmount,warnings[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/discount-reprice-snapshots/{id}` | `view` | path: id | inputs,ruleVersions,allocationBefore,allocationAfter,clawbacks,reconciliation |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/batches/preview` | `view` | items[] 增加 repricePolicyVersion | items[] 增加 discountClawbackAmount 与 repriceSnapshotId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：三科联报退一科、阶梯优惠降档、规则已下线三类算例均按原版本重放，分摊差额合计等于扣回额。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-05 · 不同事业部、岗位和责任情形采用不同扣款规则

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 未发现退款责任认定、岗位扣款规则或生效版本。
- **闭环目标：** 建立责任认定单和扣款矩阵，按业务线、岗位、原因、责任比例、生效期计算；人工认定必须审批。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、HR/退款。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`hr.page-19`（人事审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_deduction_rule_version`、`refund_responsibility_assessment`、`refund_responsibility_party`，区间和比例由数据库约束。
- **后端：** 建立按业务线、岗位、退款原因、责任类型、责任比例和生效期匹配的扣款矩阵；人工责任认定必须审批后才能进入结算。
- **前端：** 退款详情增加责任认定单和规则命中解释，审批页显示各责任人预计扣款及上限。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/deduction-rules` | `create` | businessLineIds[],positionIds[],refundReasons[],responsibilityTypes[],ratio,maxAmount,effectiveFrom,effectiveTo | ruleId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/responsibility-assessments` | `create` | refundId,parties[{employeeId,responsibilityType,ratio,evidence}],reason,idempotencyKey | assessmentId,approvalId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/responsibility-assessments/{id}/calculate` | `view` | expectedVersion,asOf | matchedRules[],partyDeductions[],totalDeduction |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨事业部、不同岗位、多人分责和规则生效日边界均命中唯一版本；未审批认定不能生成扣款。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-06 · 退费时需要看到签单、服务、教师、转课等完整上下文

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 合同、负责人、班级、教师和售后关系可追溯，但退款接口/页面未形成一次性上下文投影。
- **闭环目标：** 退款详情 API 返回只读业务快照和关键风险提示，避免财务在多个模块手工拼接。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、CRM/合同/教务/HR。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`finance.page-05`（收银与资金流水）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_context_snapshot`，每个区块保存 sourceType/sourceId/sourceVersion/capturedAt，退款提交后快照不可覆盖。
- **后端：** 在退款单上形成发生时只读上下文快照，包含签单归因、服务顾问、授课教师、班级、转课链、课消和异常风险。
- **前端：** 退款详情以单页卡片展示完整上下文和风险标签，所有来源用稳定 ID 跳转。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/refunds/{id}/context` | `view` | refresh=false | contractContext,serviceContext,teachingContext,transferContext,consumptionContext,riskFlags[],snapshotVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/refunds/{id}/context/capture` | `create` | expectedVersion,idempotencyKey | snapshotId,snapshotVersion,riskFlags[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/refunds/{id}/risk-checks` | `view` | asOf | checks[{code,severity,passed,evidenceIds[]}],blockingCount |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨校转课且更换服务顾问/教师的退款，快照能还原当时关系；主档后改不影响已提交退款。
- **前置依赖：** CON、DISC、PAY、TRANS、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-07 · 退费应自动追回已发放过渡奖励

- **补充 PRD 基线：** 未明确；🟡 部分解决。
- **最新主分支证据：** 当前只会生成通用绩效重算待办，没有过渡奖励对象和实际追回结果。
- **闭环目标：** 退款完成后调用奖励重算，生成未发放冲减或已发放追扣单，并写入薪资差异批次。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、奖励引擎、工资台账。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`hr.page-15`（课酬与佣金）、`hr.page-17`（薪资核算与审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增/复用 reward clawback 与 payroll adjustment，保证 `(refund_id, original_reward_id)` 唯一。
- **后端：** 退款完成事件调用过渡奖励重算；未发放奖励冲减冻结额，已发放奖励生成负向工资追补并用 refundId 全链路关联。
- **前端：** 退款详情展示奖励影响、追回状态和工资差异单，异常可从人力异常页重试。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/refunds/{id}/transition-reward-recalculate` | `view` | expectedVersion,idempotencyKey | recalculationId,affectedRewards[],totalClawback,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/refund-admin/refunds/{id}/transition-reward-impact` | `view` | path: id | rewards[],clawbacks[],payrollAdjustments[],reconciliationDiff |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/refunds/{id}/transition-reward-retry` | `execute` | failedStepIds[],idempotencyKey | retryJobId |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：冻结中、已确认未发、已发放三种奖励分别冲减/追回；重复退款事件不重复生成工资差异。
- **前置依赖：** TRANS-08。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### REFUND-08 · 退费应按规则自动计算教师扣款

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** HR 没有退费教师扣款规则；退款也未关联教师课酬记录并冲正。
- **闭环目标：** 定义可扣/不可扣情形、责任比例和上限；按已结课酬生成负向差异单，保留申诉和复核流程。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、教师员工端接口、后端 API、课酬/工资。
- **页面：** `after-sales.page-08`（延期停课与跨模块协同）、`hr.page-15`（课酬与佣金）、`hr.page-18`（电子工资条与异议）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V106__xuebang_refund_settlement_engine.sql。领域数据方案：新增 `refund_teacher_deduction`、`refund_teacher_deduction_line`、`refund_teacher_deduction_appeal`，引用原 teacherCompensationLineId。
- **后端：** 根据退费原因、教师责任、已结课酬、责任比例和扣款上限试算教师扣款；已结批次只生成负向差异，保留申诉复核。
- **前端：** 退款页展示逐教师试算，工资条展示来源退款；教师端可在期限内申诉并上传证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/refund/RefundAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/refundAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `aft:015-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/refunds/{id}/teacher-deductions/preview` | `view` | assessmentId,asOf | teachers[{employeeId,sourcePayLines[],ratio,grossDeduction,cappedDeduction}],total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/refunds/{id}/teacher-deductions` | `create` | previewToken,idempotencyKey | deductionBatchId,lines[],payrollAdjustmentIds[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/refund-admin/teacher-deductions/{id}/appeals` | `create` | reason,evidenceFileIds[],idempotencyKey | appealId,status,reviewDueAt |

- **最小验证：** 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不可扣、按比例、触发上限和已结课酬四类算例正确；申诉通过生成反向差异且不修改原扣款记录。
- **前置依赖：** PAY-09、REFUND-05。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 18.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-refund): close REFUND-01,REFUND-02,REFUND-03,REFUND-04,REFUND-05,REFUND-06,REFUND-07,REFUND-08`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 19: Stage 6 · 在读学员（ACTIVE-01、ACTIVE-02）

**依赖：** COURSE、ID。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V107__xuebang_active_student_metric.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 19.1：逐项差异确认与复用决策

1. 对本域 2 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 19.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V107__xuebang_active_student_metric.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 19.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 19.4：逐项接口级方案

##### ACTIVE-01 · 在读口径应为统计期间有有效课消的学员，按学员去重，并限定/排除课程类型

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** `ACTIVE_STUDENT_COUNT` 当前直接统计 `stu_student` 的 `ACTIVE/TRANSFERRED` 状态，不读取期间课消，也不处理课程类别排除，口径明确不符。
- **闭环目标：** 重写指标：按期间成功且未冲正的扣费流水关联稳定学员 ID 去重，课程经营分类配置包含/排除项，并版本化。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教务/BI、PostgreSQL。
- **页面：** `bi-dashboard.page-05`（合同收款与课消）、`bi-dashboard.page-10`（指标口径与目标）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V107__xuebang_active_student_metric.sql。领域数据方案：新增 `bi_active_student_metric_version`、`bi_active_student_contribution`，保存 consumptionId、studentId、courseCategoryVersionId、included 与 exclusionReason。
- **后端：** 重写在读指标：统计期内成功且未冲正的课消事实按稳定学员 ID 去重，并按课程经营分类版本包含或排除。
- **前端：** 指标口径页展示公式和课程分类；看板按校区/业务线/课程分类下钻到学员及课次证据。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:005-analytics-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/metrics/active-students` | `view` | period,campusIds[],businessLineIds[],courseCategoryIds[],metricVersionId | count,queryToken,includedConsumptionCount,excludedConsumptionCount |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/metrics/active-students/contributions` | `view` | queryToken,included?,page,pageSize | records,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/metrics/active-students/rebuild` | `execute` | period,metricVersionId,scope,idempotencyKey | jobId,watermark |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：同一学员多课次只计 1；冲正、引流课和排除分类不计；跨校稳定 ID 按规则去重，逐笔证据可复算。
- **前置依赖：** COURSE、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### ACTIVE-02 · 当前需手工统计，希望系统自动计算、查看明细和导出

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** BI 已有自动指标、快照和下钻框架，但由于公式错误，自动结果不能直接用于考核；专用明细导出也未闭环。
- **闭环目标：** 在正确公式上线后提供逐学员下钻、课程/课次证据、异步导出及新旧口径并行对账一个结算周期。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、BI/导出中心。
- **页面：** `bi-dashboard.page-05`（合同收款与课消）、`bi-dashboard.page-11`（自助报表）、`bi-dashboard.page-12`（数据质量与预警）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V107__xuebang_active_student_metric.sql。领域数据方案：新增 `bi_active_student_reconciliation` 和导出快照，保存 oldMetricValue/newMetricValue、difference、findingStatus。
- **后端：** 在正确口径上提供自动日/月快照、逐学员明细、异步导出，并让新旧口径并行一个结算周期形成差异表。
- **前端：** 看板显示自动更新时间、下钻、导出及新旧口径对账；未解释差异进入数据质量预警。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:005-analytics-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/metrics/active-students/reconciliations` | `view` | period,campusIds[],status,page,pageSize | records,total,summary |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：queryToken,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/metrics/active-students/reconciliations/{id}/resolve` | `view` | resolution,evidenceFileIds[],expectedVersion | status,resolvedAt |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：连续一个结算周期自动生成快照和对账；导出行数/学员 ID 与下钻一致，未处理差异阻断旧口径下线。
- **前置依赖：** COURSE、ID。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 19.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-active-students): close ACTIVE-01,ACTIVE-02`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 20: Stage 6 · 人力反馈补充（HRF-01、HRF-02、HRF-03）

**依赖：** PAY、REFUND、PERF。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V108__xuebang_hr_migration_and_workload_projection.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 20.1：逐项差异确认与复用决策

1. 对本域 3 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 20.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V108__xuebang_hr_migration_and_workload_projection.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 20.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 20.4：逐项接口级方案

##### HRF-01 · 需要把现用系统 2 的工资/人员数据迁移到新平台

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 当前代码有员工、薪资档案和工资批次，但最终验收报告明确不包含真实数据迁移；未发现系统 2 的已执行迁移结果。
- **闭环目标：** 制作字段映射、清洗规则、员工匹配键和金额对账脚本；先全量演练，再增量切换并由 HR 签字。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端迁移任务、PostgreSQL、HR/财务。
- **页面：** `hr.page-02`（员工管理）、`hr.page-17`（薪资核算与审批）、`finance.page-19`（财务异常与迁移）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V108__xuebang_hr_migration_and_workload_projection.sql。领域数据方案：新增 `migration_hr_source_record`、`migration_employee_match`、`migration_payroll_reconciliation`，原系统主键写 legacyId 并保留源行哈希。
- **后端：** 为现用系统 2 建立工资和人员数据的字段映射、清洗、员工匹配、全量演练、增量切换与金额对账流程。
- **前端：** 财务异常与迁移页提供上传、预检、匹配复核、执行和签字；HR 页面显示来源系统及迁移批次。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:002-teacher-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/migrations/system2/preview` | `view` | fileId,mappingVersion,mode=FULL\|INCREMENTAL,idempotencyKey | migrationRunId,rowCounts,validationErrors[],unmatchedEmployees[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/migrations/system2/{runId}/execute` | `execute` | approvedMappingVersion,employeeMatchDecisions[],idempotencyKey | status,inserted,updated,rejected |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/migrations/system2/{runId}/reconciliation` | `view` | path: runId | headcountDiff,payrollAmountDiff,records[],signoffStatus |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/migrations/system2/{runId}/sign-off` | `create` | hrSignerId,financeSignerId,evidenceFileIds[] | signoffStatus,signedAt |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：全量和增量各演练一次；员工数、工资批次数、应发/扣减/实发金额差异为 0 后才能双签切换。
- **前置依赖：** PAY、REFUND、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### HRF-02 · 退款后收入、业绩和工资应同步扣回

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 退款只生成财务/绩效待办，未完成净收入重算、业绩差异和工资追补闭环。
- **闭环目标：** 以退款完成事件生成净收入冲正、业绩差异和工资追补，三者使用同一退款 ID 对账。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、事件总线、财务/业绩/工资。
- **页面：** `finance.page-11`（智能会计与财务事件）、`hr.page-17`（薪资核算与审批）、`hr.page-21`（人力异常治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V108__xuebang_hr_migration_and_workload_projection.sql。领域数据方案：新增 `refund_cross_ledger_reconciliation`；各消费者以 eventId 幂等，失败进入重试/死信且不阻塞其他台账。
- **后端：** 以退款完成事件同时生成净收入冲正、业绩负向差异和工资追补，三个分录共享 refundId、eventId 和对账批次。
- **前端：** 人力异常页提供跨台账对账卡，可下钻三类分录并对单项重试。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `fin:020-smart-accounting-platform`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/refund-impacts/reconcile` | `view` | period,refundIds[],idempotencyKey | jobId |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/refund-impacts` | `view` | period,status,page,pageSize | records[{refundId,netRevenueReversal,performanceAdjustment,payrollAdjustment,difference}],total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/refund-impacts/{refundId}/repair` | `execute` | ledgerTypes[],reason,idempotencyKey | repairJobId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：退款后同一 ID 在三台账均可查，金额按规则勾稽；模拟一个消费者失败时其余已落账且修复后差异归零。
- **前置依赖：** PAY、REFUND、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### HRF-03 · 需要查看教师当前负荷和未来排课预测

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 教务有排课、教师和班级数据，但未发现教师容量模型、未来负荷预测指标或预警页面。
- **闭环目标：** 定义可授时段、最大周课时、已排/待排/请假/冲突；按未来 4–12 周输出负荷率、缺口和超载预警。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、教务/HR/BI。
- **页面：** `academic.page-05`（排课中心）、`hr.page-01`（人力工作台）、`hr.page-21`（人力异常治理）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V108__xuebang_hr_migration_and_workload_projection.sql。领域数据方案：新增 `hr_teacher_capacity_policy`、`hr_teacher_availability`、`hr_teacher_workload_forecast`，快照保存 timetableWatermark 与 leaveWatermark。
- **后端：** 建立教师可授时段、最大周课时、请假、已排和待排容量模型，滚动预测未来 4–12 周负荷率、缺口和超载。
- **前端：** 人力工作台增加负荷热力图、预测周数和阈值，异常页列出超载、低负荷与课程师资缺口。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-d/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:036-list-scheduling`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/teacher-capacity-policies` | `create` | teacherId?,positionId?,maxWeeklyMinutes,availableSlots[],effectiveFrom,effectiveTo | policyId,version |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/teacher-workload-forecasts` | `create` | weekFrom,weekCount,campusIds[],courseIds[],idempotencyKey | forecastRunId,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/hr-admin/teacher-workload-forecasts/{id}` | `view` | loadStatus[],teacherIds[],page,pageSize | records[{scheduled,pending,leave,capacity,loadRate,gap}],total,summary |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未来 4/8/12 周预测均可复算；请假和新排课触发增量更新，超载/缺口阈值预警与课表明细一致。
- **前置依赖：** PAY、REFUND、PERF。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 20.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-hr-feedback): close HRF-01,HRF-02,HRF-03`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 21: Stage 6 · 报表模块（RPT-01、RPT-02、RPT-03、RPT-04、RPT-05、RPT-06）

**依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V109__xuebang_metric_and_report_governance.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 21.1：逐项差异确认与复用决策

1. 对本域 6 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 21.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V109__xuebang_metric_and_report_governance.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 21.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 21.4：逐项接口级方案

##### RPT-01 · 运营大屏不能按实际业务展示；选择本月时指标不全、无说明、取数逻辑不清

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 新平台有指标定义、版本、快照、看板、组件和下钻；但当前只有 12 个可执行计算器，未覆盖飞书全部经营指标。
- **闭环目标：** 先由业务/财务签署“指标口径字典”，再为每个指标实现计算器、来源表、排除项、时间口径、下钻明细和样例对账。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `bi-dashboard.page-10`（指标口径与目标）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：复用 `bi_metric_definition`/`bi_metric_definition_version`，补 `formula_expression`、`source_facts`、`exclusions`、`time_semantics`、`owner_employee_id` 和示例对账表。
- **后端：** 扩展指标定义服务，指标必须绑定公式、来源事实、排除项、时间口径、维度、Owner 和生效版本；查询只能引用已发布版本。
- **前端：** 指标口径页增加版本编辑、样例计算和逐笔下钻；自助报表展示口径卡并锁定查询使用的版本。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:013-approval-change`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/bi-dashboard-admin/metrics/{id}/versions` | `view` | path: id | MetricDefinitionVersion[]，含公式、来源、排除项、生效期 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/metrics/{id}/versions` | `create` | expectedVersion, formulaExpression, sourceFacts[], exclusions[], timeSemantics, dimensions[], effectiveFrom, sampleCase | MetricDefinitionVersion |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/metrics/{id}/sample-evaluate` | `view` | metricVersionId, filters, sampleFactIds[] | value, contributions[], excludedFacts[], reconciliationDiff |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：使用已结算月份夹具断言指标值、排除项、下钻合计与样例对账完全一致。
- **前置依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### RPT-02 · 大型活动签单运营大屏不满足实际业务

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 营销模块已实现活动大屏、实时数据、人工业绩登记与排行榜；但未用本次大型活动真实字段和历史结果做 UAT。
- **闭环目标：** 以一场已结算活动回放验收：校区、课程、员工、订单、实收、退款、撤回和排名逐笔核对，形成活动模板。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、PostgreSQL/BI。
- **页面：** `marketing.page-09`（活动大屏）、`bi-dashboard.page-13`（大屏发布与轮播）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：新增 `bi_activity_dashboard_template`、`bi_activity_replay_run`、`bi_activity_replay_line`，每行保存来源业务 ID 和快照批次。
- **后端：** 为大型活动增加活动大屏模板和已结活动回放服务，固定校区、课程、员工、订单、实收、退款、撤回及排名的同源查询。
- **前端：** 活动大屏增加模板选择、结算回放、差异下钻和模板发布；差异未清零时禁止发布正式大屏。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `mkt:015-center-campaign`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/activity-replays` | `create` | activityId, templateId, settledFrom, settledTo, idempotencyKey | replayRunId, status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/activity-replays/{id}` | `view` | path: id | status, totals, rankings, reconciliationDiffs[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/activity-dashboard-templates/{id}/publish` | `execute` | expectedVersion, replayRunId | templateId, publishedVersion |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：回放固定活动夹具，订单、实收、退款、撤回和排名逐笔一致，制造一笔差异时发布返回 409。
- **前置依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### RPT-03 · 报表缺少全维度筛选

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** BI 支持集团、区域、组织、校区、员工、教师、顾问、课程、班级、课次、学员、渠道、状态等维度；缺少班课/个性化、产品线、签单类型、活动方案等业务维度。
- **闭环目标：** 建立统一维度表和维度权限；所有指标声明可筛选维度，前端按元数据生成多选筛选并校验维度组合。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、权限服务、PostgreSQL/BI。
- **页面：** `bi-dashboard.page-10`（指标口径与目标）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：新增 `bi_dimension_definition`、`bi_report_dimension_binding`、`bi_dimension_combination_rule`，保存字段类型、Lookup 类型和权限维度。
- **后端：** 建立维度目录和报表可筛维度声明，查询服务在执行前验证维度组合并叠加用户数据范围。
- **前端：** 自助报表根据 Query Schema 生成多选筛选器；无效组合在提交前提示，服务端再次校验。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:013-approval-change`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/dimensions` | `view` | reportId, keyword, page, pageSize | records, total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/query-schema` | `view` | path: id | dimensions[], requiredDimensions[], invalidCombinations[], loadPolicy |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/queries` | `create` | metricIds[], dimensions, filters, period, groupBy[], idempotencyKey | queryId, status, acceptedFilters |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：覆盖全部合法维度、非法组合、伪造校区/业务线 ID 和跨组织查询，非法请求返回 400/403。
- **前置依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### RPT-04 · 报表多为全量，考核数据仍需手工加工，筛选口径与考核机制不一致，无法一键汇总/BI

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** BI 基础设施已完成，但教师课时费、过渡奖励、退费扣款、业绩等考核指标没有可执行计算器。
- **闭环目标：** 将四类核算先落成领域台账，再接入 BI；禁止直接用通用报表替代工资/业绩结算。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、财务/人资台账、PostgreSQL/BI。
- **页面：** `bi-dashboard.page-04`（销售与顾问绩效）、`bi-dashboard.page-05`（合同收款与课消）、`hr.page-15`（课酬与佣金）、`hr.page-17`（薪资核算与审批）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：新增 `bi_settlement_ledger_binding` 和台账批次水位；每条 BI 贡献记录保存 ledgerType、ledgerId、ledgerVersion 和 settlementPeriod。
- **后端：** 报表只读取业绩、课酬、奖励、退费四类已结算台账，不直接从通用 CRUD 表拼工资或考核结果。
- **前端：** 报表详情展示来源台账和结算状态，未结算数据单独提示且不混入正式考核合计。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:004-sales-funnel`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/settlement-ledgers` | `view` | ledgerTypes[], period, campusIds[], businessLineIds[] | records, total, summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/settlement-ledgers/refresh` | `create` | ledgerTypes[], period, force, idempotencyKey | refreshJobId, watermarks[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/settlement-ledgers/{ledgerType}/{ledgerId}/contribution` | `view` | path: ledgerType, ledgerId | reportContributions[], settlementStatus |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：分别用业绩、课酬、奖励、退费已结与未结夹具断言正式报表只汇总已结台账且来源可追溯。
- **前置依赖：** PAY-01、TRANS-01、REFUND-01、PERF-01。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### RPT-05 · 统计标准不明确、解释不全，缺少报表介绍和解读

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 指标定义和版本机制已实现，可记录表达式、来源、精度和版本；但现有业务指标字典不完整。
- **闭环目标：** 每张报表固定展示“用途、口径、公式、更新时间、来源、排除项、负责人、版本、生效日”，并提供口径变更审批。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、审批中心、PostgreSQL/BI。
- **页面：** `bi-dashboard.page-10`（指标口径与目标）、`bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：扩展 `bi_metric_definition_change`，保存 before/after、影响报表、审批实例 ID 和生效版本。
- **后端：** 为每张报表维护用途、口径、公式、更新时间、来源、排除项、负责人、版本和生效日，变更通过审批后发布。
- **前端：** 报表标题旁固定提供“口径说明”；指标口径页提供变更差异、审批和历史版本回放。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:013-approval-change`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/definition` | `view` | path: id, version? | purpose, formula, refreshPolicy, sources[], exclusions[], owner, version, effectiveFrom |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/report-definition-changes` | `create` | reportId, beforeVersion, changes, effectiveFrom, reason, idempotencyKey | changeId, approvalId, status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/report-definition-changes/{id}/publish` | `execute` | expectedVersion, approvalId | reportDefinitionVersion |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：断言未审批变更不能发布，已发布版本在历史查询中可回放，页面说明与 API 定义完全一致。
- **前置依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### RPT-06 · 点进报表即刷新、卡顿；应先选条件再查询

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 通用资源页和 BI 专页均会在进入页面时自动加载，未按大数据报表实施“查询后加载”。
- **闭环目标：** 报表元数据增加 `loadPolicy`；高聚合报表默认空态，用户选周期/组织/产品线后点击查询，后端异步计算并缓存。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、Redis、PostgreSQL/BI。
- **页面：** `bi-dashboard.page-11`（自助报表）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V109__xuebang_metric_and_report_governance.sql。领域数据方案：为查询任务保存 filterHash、metricVersion、scopeHash、status、progress、resultFileId、expiresAt；缓存键包含权限范围。
- **后端：** 按报表元数据执行 MANUAL/ASYNC/CACHED 加载策略，高聚合报表初始只返回 Schema，提交后异步计算并缓存结果。
- **前端：** 报表页面进入时不请求事实数据；查询按钮提交任务，显示进度、取消、失败重试和缓存命中时间。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/bi/BiDashboardService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/biDashboardAdmin.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-e/providers.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `bi:039-reports`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/reports/{id}/query-schema` | `view` | path: id | loadPolicy, requiredFilters[], maxSyncDays, cacheTtlSeconds |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/bi-dashboard-admin/reports/{id}/query-jobs` | `view` | filters, period, groupBy[], idempotencyKey | jobId, status, cacheHit |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/bi-dashboard-admin/report-query-jobs/{id}` | `view` | path: id | status, progress, result, error, expiresAt |

- **最小验证：** 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：Playwright 断言首屏零事实请求；同用户同条件命中缓存，不同权限用户不能复用越权缓存。
- **前置依赖：** TARGET、PERF、ACTIVE、LIST、QUERY。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 21.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-reports): close RPT-01,RPT-02,RPT-03,RPT-04,RPT-05,RPT-06`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 22: Stage 7 · 优化建议（OPT-01、OPT-02、OPT-03、OPT-04、OPT-05、OPT-06、OPT-07）

**依赖：** ID、LIST。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V110__xuebang_bulk_change_and_fact_snapshot.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 22.1：逐项差异确认与复用决策

1. 对本域 7 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 22.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V110__xuebang_bulk_change_and_fact_snapshot.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 22.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 22.4：逐项接口级方案

##### OPT-01 · 学员与客户可批量修改年级、性别、公立学校

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 现有 CRM/学员服务以单条新增修改为主，未发现带权限、校验和回滚结果的批量修改。
- **闭环目标：** 提供“筛选选中/导入模板”两种批改；先预检差异，逐条返回成功失败，敏感字段需审批并审计。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、审批中心、CRM/学员。
- **页面：** `crm.page-03`（客户档案）、`platform.page-05`（审批流程）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：新增 `crm_student_bulk_change`、明细和 before/after 审计，单行独立结果且批次可重试失败项。
- **后端：** 提供筛选选中和模板导入两种批量修改，年级、性别、公立学校先预检差异，再逐条执行；跨校和敏感字段按策略审批。
- **前端：** 学员/客户列表增加批量修改向导，展示预计影响、校验错误、审批要求和结果下载。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/student-bulk-changes/preview` | `view` | selection{studentIds[]\|queryToken},changes{grade?,gender?,publicSchoolId?} | previewToken,affectedCount,rows[],approvalRequired |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/student-bulk-changes` | `create` | previewToken,reason,evidenceFileIds[],idempotencyKey | bulkChangeId,approvalId?,status |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/crm-admin/student-bulk-changes/{id}/execute` | `execute` | expectedVersion,idempotencyKey | status,succeededCount,failedCount,resultFileId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：200 条含合法、越权、无效学校和并发变更数据逐条返回结果；成功项有审计，失败重试不重复成功项。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-02 · 扣费产生时快照学员年级、教师全兼职、班级/课程/教师/合同 ID，并在导出增加课程分类

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 扣费关系保留部分 ID，但未发现完整业务快照，教师身份和学员年级可能随主档变化；专用导出也未覆盖课程分类。
- **闭环目标：** 在扣费事实表保存发生时快照和全部来源 ID，课程分类保存 ID+名称+版本；历史报表只读快照。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 后端 API、PostgreSQL、课消/导出、BI。
- **页面：** `academic.page-11`（考勤与课消）、`bi-dashboard.page-05`（合同收款与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：扩展 consumption fact 的 snapshot 列和 `course_category_version_id`，新增完整性约束与历史回填异常表。
- **后端：** 扣费发生时冻结学员年级、教师全兼职、班级/课程/教师/合同/权益 ID 和课程分类 ID+名称+版本，历史报表只读快照。
- **前端：** 扣费详情显示“发生时快照”，导出增加课程分类及全部来源 ID；当前主档值只作对比。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:003-attendance-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/consumptions` | `create` | lessonId,studentId,entitlementId,idempotencyKey | consumptionId,snapshot{studentGrade,teacherEmploymentType,classId,courseId,teacherEmployeeId,contractId,entitlementId,courseCategory} |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/consumption-records` | `view` | filters,page,pageSize | records 含完整 snapshot 与 snapshotVersion |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：扣费后修改年级、教师身份、课程分类和名称，历史 API/导出仍返回发生时值；缺快照事实被阻断或列入修复。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-03 · 在班学员按班级状态筛选已结课/上课中/未开课

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 班级自身已有状态字段，可作为筛选基础；当前在班学员列表是否完整接入三态多选尚未形成专用契约。
- **闭环目标：** 在班学员查询接收 `classStatuses[]`，状态由开课/结课日期和业务动作统一维护，并加三态测试。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、班级/在班学员。
- **页面：** `academic.page-04`（班级与学员）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：新增/修正 class_status_projection 和状态迁移审计，禁止前端自行根据日期推断不同口径。
- **后端：** 在班学员查询接收 NOT_STARTED/IN_PROGRESS/COMPLETED 多选，班级状态由业务动作和开结课日期统一投影。
- **前端：** 在班学员增加班级状态远程多选、标签和状态更新时间。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:041-class-split-merge`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/class-students` | `view` | classStatuses[],asOf,page,pageSize | records 含 classStatus,classStatusChangedAt,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/classes/{id}/status-history` | `view` | path: id | records[{fromStatus,toStatus,occurredAt,reason,sourceEventId}] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/classes/status-projections/rebuild` | `execute` | classIds[],asOf,idempotencyKey | jobId,acceptedCount |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未开课、上课中、已结课和组合筛选四组结果正确；未传/空数组语义一致，投影与班级详情状态相同。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-04 · 班级管理增加“该班在指定日期内有排课”的上课日期筛选

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 班级与课次有关系，但班级列表查询未发现按课次日期 `exists` 筛选。
- **闭环目标：** 增加 `lessonDateFrom/To`，用课次表 `EXISTS` 查询并建 `(class_id,start_at,status)` 索引。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、排课/班级、PostgreSQL。
- **页面：** `academic.page-01`（教务工作台）、`academic.page-05`（排课中心）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：为 lesson 建 `(class_id,start_at,status)` 索引，查询排除 CANCELLED/DELETED 并按校区时区解析边界。
- **后端：** 班级列表增加 lessonDateFrom/lessonDateTo，使用有效课次 EXISTS 判断指定日期内是否排课，不因多课次产生重复班级。
- **前端：** 班级管理增加“上课日期”范围和自然周/月快捷筛选，显示命中课次数。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:002-schedule-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/academic-admin/classes` | `view` | lessonDateFrom,lessonDateTo,lessonStatuses[],page,pageSize | records 含 matchedLessonCount,total,summary |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/classes/{id}/lessons-in-period` | `view` | from,to,statuses[],page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/classes/query-plan-check` | `view` | lessonDateFrom,lessonDateTo,otherFilters | planHash,indexUsed,estimatedRows |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：有课、只有已取消课、跨日课和边界课次四类班级结果正确；EXPLAIN 使用目标索引且分页班级不重复。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-05 · 员工管理导出增加员工 ID，主要用于教师匹配

- **补充 PRD 基线：** 明确覆盖；❌ 未解决。
- **最新主分支证据：** 当前 HR 员工导出输出编号、姓名等字段，但未包含员工主键 ID。
- **闭环目标：** 增加 `employeeId` 并将其列为首列；同步补到考勤、扣费、课酬和工资明细导出。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、HR/导出。
- **页面：** `hr.page-02`（员工管理）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：统一 HR employee identity 导出 Schema，所有引用员工的资源必须绑定该字段组。
- **后端：** 员工导出将 employeeId 作为首列，并同步到考勤、扣费、课酬和工资明细导出，员工编号与姓名随后展示。
- **前端：** 员工管理导出列默认勾选员工 ID，相关业务导出不能取消关键 ID 列。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminMetadataController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `hr:002-teacher-hr`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/admin/metadata/business-fields/hr.employee.identity` | `view` | version? | fields[{employeeId,employeeCode,employeeName,requiredInExport}] |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters,columns[] | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/hr-admin/employee-reference-exports/validate` | `export` | resourceKey,columns[] | valid,normalizedColumns[],errors[] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：员工、考勤、扣费、课酬、工资五类导出首个身份字段均为 employeeId；同名教师匹配不串人。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-06 · 课程有效/无效支持多选；导入和手工新增时课程名称唯一

- **补充 PRD 基线：** 部分覆盖；🟡 部分解决。
- **最新主分支证据：** 课程有状态和唯一课程编码，但筛选主要为单值，数据库也未约束课程名称唯一。
- **闭环目标：** 状态改数组筛选；明确名称唯一范围（集团或校区+业务线），导入与接口共用同一重复校验并提供冲突合并流程。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、后端 API、课程/导入、PostgreSQL。
- **页面：** `academic.page-02`（课程产品）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：增加 normalized_name、唯一约束和 course_merge_request；名称规范化处理空格/大小写但不错误折叠语义不同课程。
- **后端：** 课程状态改为数组筛选；课程名称在“集团+业务线+学段”规范化范围唯一，手工新增和导入共用同一重复校验及合并流程。
- **前端：** 课程页支持有效/无效多选，新增和导入在提交前显示冲突课程并允许发起合并审批。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:032-course`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 主分支已有，仅按缺口扩展 | `GET` | `/api/v1/academic-admin/courses` | `view` | statuses[],businessLineIds[],schoolStages[],keyword,page,pageSize | records,total |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/courses/duplicate-check` | `create` | name,businessLineId,schoolStage,excludeCourseId? | normalizedName,conflicts[],canCreate |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/courses/imports/preview` | `view` | fileId,mappingVersion | validRows[],duplicateRows[],mergeCandidates[],errors[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/course-merge-requests` | `create` | sourceCourseIds[],targetCourseId,reason,idempotencyKey | requestId,approvalId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：单状态、多状态、空状态语义正确；手工和导入对同名同范围均阻断，跨业务线合法，合并保留全部历史 ID 映射。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### OPT-07 · 每校区每课程价格不同，需要可导出的价格明细

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 价格方案能关联校区与课程，但专用价格明细导出未实现。
- **闭环目标：** 增加价格方案异步导出，包含校区/课程/产品线 ID、定价模式、总价、课时、单价、有效期和状态。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、定价/导出中心。
- **页面：** `contract.page-01`（价格与套餐）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V110__xuebang_bulk_change_and_fact_snapshot.sql。领域数据方案：新增价格导出 Schema 与快照，保存 pricePlanVersion、filterHash、scopeHash 和 generatedAt。
- **后端：** 实现校区课程价格方案异步导出，固定包含校区/课程/产品线 ID、定价模式、总价、课时、单价、有效期和状态。
- **前端：** 价格套餐页增加多维筛选、导出列预览和异步任务状态。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/hr/HrAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/hrAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoDataTable.vue`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:004-multi-subject-package`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：campusIds[],courseIds[],productLineIds[],statuses[],effectiveAt?,columns[],idempotencyKey | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |
| 复用 | 主分支已有，直接复用 | `GET` | `/api/v1/admin/{moduleBase}/{resourceCode}/import-export-tasks/{taskId}` | `view` | path: moduleBase/resourceCode/taskId | 通用导出任务状态、进度、行数、结果文件和错误信息 |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/contract-admin/price-plans/export-schema` | `view` | version? | columns[{key,label,type,required}] |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同课程多校区不同价逐行正确；总价÷课时与单价按精度规则对账，页面筛选与导出 filterHash 一致。
- **前置依赖：** ID、LIST。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 22.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-optimizations): close OPT-01,OPT-02,OPT-03,OPT-04,OPT-05,OPT-06,OPT-07`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 23: Stage 7 · 已发现缺陷（BUG-01、BUG-02、BUG-03、BUG-04、BUG-05、BUG-06）

**依赖：** CRM、CON、COURSE、ID、OPT。

**Files:**

- Conditional migration: `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V111__xuebang_known_defect_regression_facts.sql`（仅实际 Schema 差异存在时创建或合并）
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`
- Candidate backend: `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`
- Candidate frontend: `SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`
- Tests: 优先扩展现有相邻测试；没有合适承载位置时才创建一个必要层级的用例，本域不默认创建独立 E2E 套件。

#### Task 23.1：逐项差异确认与复用决策

1. 对本域 6 项逐项比较最新 Controller、Service、Models、Flyway、页面 provider、现有测试和 OpenAPI，登记 REUSE_EXISTING、UAT_FIRST、EXTEND_EXISTING 或 IMPLEMENT_GAP。
2. 先检查 GenericAdmin、AdminLookup、Runtime Task/Event/Approval、SystemOps、审计、幂等和导入导出；能复用时记录 operation，不新增平行 Controller、表或任务中心。
3. 已解决项只核对缺失边界；待验收项先使用真实数据 UAT。只有证据证明失败时才转为代码任务。

#### Task 23.2：最小实现

1. 只有表、字段、约束、索引或历史映射确实缺失时才实现 `SourceCode/dinuo-admin-api/src/main/resources/db/migration/V111__xuebang_known_defect_regression_facts.sql`；同一依赖组优先合并，禁止空迁移。
2. API 先扩展已有 DTO/Service/Controller；只有现有 operation 无法表达领域语义、权限或事务边界时才新增，并记录 reuseRejectedReason。
3. 只有跨模块、异步或财务副作用写入才使用 Outbox；同步页面、查询、Lookup、标签和普通主数据维护不增加事件链。
4. 前端只修改实际受影响页面和 provider，不为每项复制 API client、状态机、导出页或进度页。

#### Task 23.3：按风险选择验证

1. 后端规则、事务、锁或约束变化时运行相邻 JUnit/PostgreSQL 测试；前端适配变化时运行相邻 Vitest；只有关键跨模块用户路径进入 Playwright。
2. Controller/DTO 改动时才运行 OpenAPI 生成和一致性审计；无接口改动的 UAT/UI 项不生成契约噪音。
3. 金额、计数、课时、结算和历史口径必须做固定数据集对账；纯布局、文案和选择器问题不做财务对账。
4. 本域提交前只跑受影响测试；全量后端、前端和既有发布门禁在最终发布候选统一运行一次。

#### Task 23.4：逐项接口级方案

##### BUG-01 · 客户跟进状态全选与全不选结果不一致；不勾选会漏已成交/已试听

- **补充 PRD 基线：** 未明确；❌ 未解决。
- **最新主分支证据：** 新 CRM 尚未发现针对“空筛选=全部”与“显式全选=全部”的查询契约和回归测试。
- **闭环目标：** 统一空值、空数组和全量枚举语义；前后端共享状态枚举，增加全不选、全选、部分选三组测试。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、CRM。
- **页面：** `crm.page-04`（跟进与公海）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V111__xuebang_known_defect_regression_facts.sql。领域数据方案：查询规范化层记录 normalizedFilters，移除把空数组解释为无结果的旧分支。
- **后端：** 统一跟进状态未传、null、空数组和显式全枚举为不限制；部分枚举才生成 IN 条件，前后端共享同一状态目录。
- **前端：** 状态多选提供全选/清空，清空后显示全部而非漏掉已成交和已试听。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/AdminModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/admin/GenericAdminService.java`；前端 `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/crmAdmin.ts`、`SourceCode/dinuo-admin-vben/src/api/admin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:003-follow-tasks`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/follow-up-statuses` | `view` | includeInactive=false | records[{code,label,sortNo}] |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/crm-admin/follow-ups` | `view` | statuses[]?,page,pageSize | records,total,normalizedFilters |
| 复用 | 主分支已有，直接复用 | `POST` | `/api/v1/admin/{moduleBase}/{resourceCode}/export` | `export` | path: moduleBase/resourceCode；filters,columns,filterHash,scopeHash,idempotencyKey；原业务条件：filters{statuses[]?},columns[] | taskId,taskNo,status,totalCount,resultFileId；进度与文件复用通用任务能力 |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：不传、null、空数组、全选四组返回相同全集并包含已成交/已试听，部分选只返回目标状态；导出一致。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### BUG-02 · 学员收费优惠/优惠券太多，缺少搜索且只能单选

- **补充 PRD 基线：** 明确覆盖；🟡 部分解决。
- **最新主分支证据：** 服务端已支持名称搜索和多规则报价；部分管理端表单仍为单值/全量加载，实际收费端尚未完成本次 UAT。
- **闭环目标：** 所有制单入口统一为分页远程搜索多选；删除单值兼容路径并做大规则量端到端验收。
- **实施模式：** `EXTEND_EXISTING`。
- **改动端：** 管理端 PC、员工制单端接口、后端 API、优惠。
- **页面：** `contract.page-02`（优惠规则）、`contract.page-05`（员工端制单审计）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V111__xuebang_known_defect_regression_facts.sql。领域数据方案：优惠 Lookup 增加关键词/状态/适用课程索引并限制分页，废弃 singleDiscountRuleId 字段。
- **后端：** 所有收费入口统一使用优惠规则分页远程搜索多选，删除单值和全量加载兼容路径，报价服务接收 discountRuleIds[]。
- **前端：** 制单和优惠弹窗共用 DinuoEntityPicker，多选项显示规则类型、生效期和可叠加性。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupModels.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/lookup/AdminLookupService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/adminLookups.ts`、`SourceCode/dinuo-admin-vben/src/api/contractAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `ctr:002-discount-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 复用 | 主分支已有，直接复用 | `GET` | `/api/v1/admin/lookups/{entityType}` | `view` | entityType 按资源注册；keyword,statuses[],courseIds[],selectedIds[],page,pageSize | records,total |
| 修改 | 主分支已有，仅按缺口扩展 | `POST` | `/api/v1/contract-admin/quote/calculate` | `view` | courseItems[],discountRuleIds[],promotionContext | matchedRules[],rejectedRules[],allocations[],totalPayable |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/contract-admin/orders` | `create` | quoteId,discountRuleIds[],items[],idempotencyKey | orderId,quoteSnapshotId |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：一万条规则数据下远程搜索 P95 达门槛；多选叠加/冲突解释正确，任何入口不再发送单值字段。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### BUG-03 · 学员列表“录入日期/入学日期、班级/班别”在查询与导出名称不一致

- **补充 PRD 基线：** 未明确；⚪ 待业务验收。
- **最新主分支证据：** 当前未取得新平台同一筛选条件下的页面与导出成品；静态代码不足以证明列名和含义一致。
- **闭环目标：** 建立字段字典，页面列、查询项、API 字段和导出表头由同一元数据生成；用快照测试锁定名称。
- **实施模式：** `UAT_FIRST`。
- **改动端：** 管理端 PC、导出。
- **页面：** `crm.page-03`（客户档案）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：默认不建表；字段 key 与中文标签优先由现有 pageCatalog、DTO 和导出 Schema 维护。只有多个运行时消费者确需动态配置且现有结构无法承载时才评审元数据表。
- **后端：** 先用同一筛选条件核对现有学员列表 API 与导出；若名称不一致，修改现有 DTO/导出映射，不建设新的字段字典服务。
- **前端：** 验收失败时统一现有查询项、列配置和导出表头；不新增字段字典页面。
- **候选落地文件：** 后端 无默认改动；前端 无默认改动；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `crm:002-customer-profile`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

- **接口级决策：** 先验收现有列表与通用导出；失败时扩展现有 operation，不新增 field-dictionary、students 或 students/export 平行路由。

- **最小验证：** 先做一次同数据集页面/导出 UAT；失败修复后增加一条共享字段标签契约测试，不新建 JUnit、Vitest、Playwright 三套测试。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### BUG-04 · 排课次数不足时删排课，班级结课日期不变化；应去掉限制并重算

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 未发现删除/取消课次后自动重算班级最后上课日和结课日的闭环测试。
- **闭环目标：** 班级结课日从有效课次投影计算；课次增删改事件触发重算，手工锁定时提示差异并留审计。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、排课/班级。
- **页面：** `academic.page-01`（教务工作台）、`academic.page-05`（排课中心）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V111__xuebang_known_defect_regression_facts.sql。领域数据方案：新增 `academic_class_date_projection` 和重算审计；手工锁定结课日时保存差异、原因和审批状态。
- **后端：** 取消“排课次数不足不可删除”的旧限制；课次新增、修改、取消、删除后按有效课次重算班级最后上课日和投影结课日。
- **前端：** 删除/取消课次确认框展示重算前后日期；班级详情显示系统投影与手工锁定差异。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:002-schedule-engine`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 修改 | 候选新增；开工前必须证明现有接口无法承载 | `DELETE` | `/api/v1/academic-admin/lessons/{id}` | `delete` | expectedVersion,reason,idempotencyKey | deletedLessonId,classDateProjection{lastLessonAt,projectedEndAt},warnings[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/classes/{id}/date-projection/rebuild` | `execute` | reason,idempotencyKey | projectionVersion,lastLessonAt,projectedEndAt,differenceToLockedDate |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `PUT` | `/api/v1/academic-admin/classes/{id}/end-date-lock` | `edit` | expectedVersion,lockedEndAt,reason,approvalInstanceId? | lockVersion,difference |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：删除首/中/末课次和全部课次均可执行并正确重算；手工锁定不被覆盖但差异有审计和提示。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### BUG-05 · 学员出班后历史月份“应到人数”随当前名单变化，数据未锁定

- **补充 PRD 基线：** 部分覆盖；❌ 未解决。
- **最新主分支证据：** 当前关系能记录出勤，但没有证据表明课次应到名单在点名/关课时形成不可变快照。
- **闭环目标：** 课次发布或点名时生成应到学员快照；后续出班只影响未来课次，历史修正走有原因的版本变更。
- **实施模式：** `IMPLEMENT_GAP`。
- **改动端：** 管理端 PC、后端 API、考勤/班级、PostgreSQL。
- **页面：** `academic.page-11`（考勤与课消）。
- **数据库决策：** 先与 V1-V89 实际表结构做差异；仅缺字段、约束、索引或台账时才创建/合并到候选迁移 V111__xuebang_known_defect_regression_facts.sql。领域数据方案：新增 `attendance_expected_roster_snapshot`、版本和明细，唯一键 lessonId+version，关课版本不可直接更新。
- **后端：** 课次发布或首次点名时生成不可变应到学员快照；出班只影响未来课次，历史修正需带原因生成新版本。
- **前端：** 考勤页显示应到快照版本、生成时间和差异；历史修正通过专用流程而非读取当前班级名单。
- **候选落地文件：** 后端 `SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/crm/CrmAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/contract/ContractAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminService.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminController.java`、`SourceCode/dinuo-admin-api/src/main/java/com/dinuo/admin/academic/AcademicAdminModels.java`；前端 `SourceCode/dinuo-admin-vben/src/modules/batch-a/pageCatalog.ts`、`SourceCode/dinuo-admin-vben/src/modules/batch-a/providers.ts`、`SourceCode/dinuo-admin-vben/src/components/business/DinuoSearchPanel.vue`、`SourceCode/dinuo-admin-vben/src/api/academicAdmin.ts`；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:003-attendance-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

| 接口决策 | 主分支复核 | Method | Path | 权限动作 | Request | Response |
| --- | --- | --- | --- | --- | --- | --- |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/lessons/{id}/expected-roster/capture` | `create` | trigger=PUBLISH\|FIRST_ROLL_CALL,idempotencyKey | snapshotId,version,expectedCount,studentIds[] |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `GET` | `/api/v1/academic-admin/lessons/{id}/expected-roster` | `view` | version? | snapshotId,version,expectedStudents[],expectedCount,capturedAt |
| 新增 | 候选新增；开工前必须证明现有接口无法承载 | `POST` | `/api/v1/academic-admin/lessons/{id}/expected-roster/revisions` | `create` | baseVersion,addStudentIds[],removeStudentIds[],reason,evidenceFileIds[] | revisionId,approvalId?,newVersion |

- **最小验证：** 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：历史月份学员出班前后应到人数不变；未来课次名单更新，历史修正保留旧版本且报表引用指定版本。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

##### BUG-06 · 扣费页面“课程类型/扣费来源”与导出“上课类型/来源”命名不一致

- **补充 PRD 基线：** 未明确；⚪ 待业务验收。
- **最新主分支证据：** 新平台尚无经过验收的扣费专用导出，无法确认该显示缺陷已消失，也不能据此判为已解决。
- **闭环目标：** 课程类型、上课类型、扣费来源分别定义独立字段；页面与导出共用 Schema，并用表头契约测试防回归。
- **实施模式：** `UAT_FIRST`。
- **改动端：** 管理端 PC、导出。
- **页面：** `academic.page-11`（考勤与课消）、`platform.page-07`（文件与导入导出）。
- **数据库决策：** 默认不创建迁移；只有验证出的真实 Schema/索引缺口才进入本域候选迁移。领域数据方案：默认不迁移；课程类型、上课类型、扣费来源继续使用现有字段，仅在确有字段混用数据时执行一次性修复并生成差异清单。
- **后端：** 先核对现有扣费列表和导出字段；若名称或含义不一致，统一现有 DTO、枚举和导出映射，不建设新的字段目录服务。
- **前端：** 验收失败时统一现有表格、筛选、详情和导出预览标签，不新增字段目录页面。
- **候选落地文件：** 后端 无默认改动；前端 无默认改动；迁移和测试均按实际差异选择，不要求创建本域专用文件。
- **权限基线：** 页面入口权限 `edu:003-attendance-consumption`；每个实际使用或新增接口按动作校验，并叠加有效数据范围。

- **接口级决策：** 先验收现有扣费列表与通用导出；失败时修改现有 operation，不新增 business-fields 或 consumption-records 平行路由。

- **最小验证：** 先做页面/API/导出同数据集 UAT；失败修复后与 BUG-03 共用一条字段标签契约测试，不增加独立 E2E。
- **前置依赖：** CRM、CON、COURSE、ID、OPT。
- **完成定义：** 进度台账记录实施模式、复用/新增 operation、实际改动文件、选中的必要测试和 UAT 结果；未发生的迁移、接口或测试留空并注明“不适用”，不得为了填字段制造实现。

#### Task 23.5：本域提交检查

- 必查：本域所有 ID 已登记实施模式，REUSE_EXISTING/UAT_FIRST 有证据，新增接口有 reuseRejectedReason。
- 后端改动：只运行受影响 JUnit/PostgreSQL 测试；前端改动：只运行受影响 Vitest、typecheck 或 build。
- Controller/DTO 改动：运行 API catalog、OpenAPI 生成和契约一致性审计；没有接口改动则不运行。
- 本域默认不新建、不运行独立 Playwright；仅把关键场景纳入最终五条跨模块 E2E。

建议提交信息：`feat(xuebang-bugs): close BUG-01,BUG-02,BUG-03,BUG-04,BUG-05,BUG-06`。提交只包含实际改动、必要测试和进度证据，不要求无关迁移、契约或测试文件。

### Task 24: Stage 8 · 132 项证据台账、全量联调与发布门禁

**Files:**

- Create: `docs/01阶段技术开发/学邦132项开发进度.json`
- Create: `scripts/audit_xuebang_132_coverage.mjs`
- Create: `scripts/tests/audit_xuebang_132_coverage.test.mjs`
- Create or extend: `SourceCode/dinuo-admin-vben/e2e/xuebang-core-business-flows.spec.ts`（只承载五条跨模块主链）
- Modify: `SourceCode/dinuo-admin-vben/package.json`（只接入最终保留的共享 E2E，不接入 23 个域套件）
- Modify: `SourceCode/dinuo-infra/validate-admin-stack.sh`
- Modify: `docs/01阶段技术开发/管理员后台页面业务闭环验收矩阵.json`

#### Task 24.1：建立机器可审计进度台账

交付证据只保存在版本库 JSON、CI 产物和 UAT 文件中，不进入生产数据库。`学邦132项开发进度.json` 使用同一 132 ID，至少记录 implementationMode、status、reusedOperations、newOperations、reuseRejectedReason、changedFiles、migrationFiles、selectedTests、reconciliationEvidence、uatEvidence 和 updatedAt。未发生的层级使用空数组并注明 notApplicableReason。

审计脚本必须同时比较：

1. 原始核对报告中的 132 ID；
2. 本开发计划中的 132 ID；
3. 进度 JSON 的 132 ID；
4. 实际复用或新增的 OpenAPI operation；
5. 实际创建的 Flyway 迁移及其依赖顺序；
6. selectedTests 指向的测试文件和 CI 结果；
7. 验收矩阵、对账和 UAT 证据。

任何缺失、重复、未知 ID、状态超前、证据文件不存在、新接口没有 reuseRejectedReason、或声明新增的 API 未进入 OpenAPI 都必须非零退出；不要求每个 ID 同时具备迁移、JUnit、Vitest 和 Playwright。

#### Task 24.2：迁移双路径验证

1. 若本次存在新迁移，空库从 V1 执行到本次实际最后迁移版本，验证新增约束、索引和种子；没有新迁移则本步骤不适用。
2. 若涉及历史数据或 Schema 变化，从匿名化 V89 生产快照升级到实际最后迁移版本并运行回填、差异表和台账对账；纯前端/API 扩展不跑该路径。
3. 只有包含大表回填、不可逆状态变化或高风险约束时才做迁移中断恢复演练；恢复使用前滚修复迁移。
4. 系统 2 的 HR/工资数据属于明确迁移范围时，才执行全量演练、增量切换、员工匹配和 HR/财务双签。

#### Task 24.3：发布候选一次性门禁

```bash
cd /Users/ethan/Project/Dinuo-xuebang-132
node scripts/audit_xuebang_132_coverage.mjs --check
node scripts/generate_admin_api_catalog.mjs --check
node scripts/generate_admin_openapi.mjs --check
node scripts/audit_admin_api_contract_consistency.mjs --check
cd SourceCode/dinuo-admin-api
mvn -q test
mvn -q -DskipTests package
cd ../dinuo-admin-vben
corepack pnpm test
corepack pnpm typecheck
corepack pnpm build
corepack pnpm test:e2e:release
```

上述全量门禁只在所有域合并完成并形成发布候选后运行一次；日常提交只运行受影响测试。只有 LOAD、QUERY、ASSET 或明确性能指标发生变化时，才额外执行 `RUN_CAPACITY_GATE=1 bash SourceCode/dinuo-infra/validate-admin-stack.sh`。

#### Task 24.4：业务回放、对账和 UAT

- 共享 E2E 只覆盖五条主链：签约/收款/业绩、教学/课消/课酬、退费/追回/冲正、员工异动/权限、指标/报表/导出。
- 使用至少一个已结历史月回放客户→合同→收款→课消→课酬→业绩→奖励→退款→工资→BI 全链路。
- 财务金额逐笔对账差异为 0；人数、课时、课程项和状态计数差异逐项解释并签字。
- 以集团、区域、校区、业务线、学段、HR、财务、教务、销售、教师等角色执行权限和导出越权矩阵。
- 132 项逐项由业务 Owner 验收，不能用“模块整体通过”替代单项签字。
- 只有进度台账 132 项全部为 `uat_passed`，全栈门禁通过且无未解释差异时才可生成发布候选。

#### Task 24.5：主分支漂移处理与发布

发布候选前再次 `git fetch origin main`。若主分支已推进，先在开发分支合并最新主分支并重新检查实际改动；迁移只为仍存在的 Schema 差异分配最新可用版本，不为保持候选 V90-V111 连号而创建文件。随后重跑 Task 24.3 一次性门禁。

建议最终提交信息：`feat(xuebang): deliver all 132 supplemental PRD closures`。

## 4. 132 项追踪索引

| ID | 问题域 | 实施模式 | 原问题 | Stage | 页面 | 接口引用数 | 最小验证 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| RPT-01 | 报表模块 | `EXTEND_EXISTING` | 运营大屏不能按实际业务展示；选择本月时指标不全、无说明、取数逻辑不清 | 6 | bi-dashboard.page-10、bi-dashboard.page-11 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：使用已结算月份夹具断言指标值、排除项、下钻合计与样例对账完全一致。 |
| RPT-02 | 报表模块 | `EXTEND_EXISTING` | 大型活动签单运营大屏不满足实际业务 | 6 | marketing.page-09、bi-dashboard.page-13 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：回放固定活动夹具，订单、实收、退款、撤回和排名逐笔一致，制造一笔差异时发布返回 409。 |
| RPT-03 | 报表模块 | `EXTEND_EXISTING` | 报表缺少全维度筛选 | 6 | bi-dashboard.page-10、bi-dashboard.page-11 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：覆盖全部合法维度、非法组合、伪造校区/业务线 ID 和跨组织查询，非法请求返回 400/403。 |
| RPT-04 | 报表模块 | `IMPLEMENT_GAP` | 报表多为全量，考核数据仍需手工加工，筛选口径与考核机制不一致，无法一键汇总/BI | 6 | bi-dashboard.page-04、bi-dashboard.page-05、hr.page-15、hr.page-17 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：分别用业绩、课酬、奖励、退费已结与未结夹具断言正式报表只汇总已结台账且来源可追溯。 |
| RPT-05 | 报表模块 | `EXTEND_EXISTING` | 统计标准不明确、解释不全，缺少报表介绍和解读 | 6 | bi-dashboard.page-10、bi-dashboard.page-11 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：断言未审批变更不能发布，已发布版本在历史查询中可回放，页面说明与 API 定义完全一致。 |
| RPT-06 | 报表模块 | `IMPLEMENT_GAP` | 点进报表即刷新、卡顿；应先选条件再查询 | 6 | bi-dashboard.page-11 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：Playwright 断言首屏零事实请求；同用户同条件命中缓存，不同权限用户不能复用越权缓存。 |
| CRM-01 | 客户管理 | `EXTEND_EXISTING` | 记录客户被分配次数、最后分配人和日期 | 3 | crm.page-03、crm.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：构造三次分配事件，断言列表、详情和导出均返回 3 及最新人员/日期。 |
| CRM-02 | 客户管理 | `EXTEND_EXISTING` | 客户可释放或转到其他校区；A 校登记、B 校成交时一线或行政可处理 | 3 | crm.page-06、crm.page-03 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖 A 校登记/B 校成交、审批拒绝、执行中单项失败回滚和合同稳定 ID 不变。 |
| CRM-03 | 客户管理 | `IMPLEMENT_GAP` | 班课资源一个月未成交后自动提醒个性化部门跟进 | 3 | crm.page-04、crm.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一规则同一窗口重复扫描只生成一个任务；成交、C 类无效和已转派客户不再生成。 |
| CRM-04 | 客户管理 | `IMPLEMENT_GAP` | 转化率=期间转化成功÷（期间新增+期间再分配），不含 C 类无效客户 | 3 | crm.page-09、bi-dashboard.page-03 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：用新增、再分配、成功、C 类无效四类事件固定算例断言结果和事件时间边界。 |
| CRM-05 | 客户管理 | `IMPLEMENT_GAP` | 客户列表除首单日期、课程、金额外增加首单合同 ID | 3 | crm.page-03 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖已盖章已支付、未盖章、撤销、全退和多孩子家庭，首单关系不串档。 |
| CRM-06 | 客户管理 | `EXTEND_EXISTING` | 手机号唯一导致老资源/渠道冲突；需激活老资源并记录后期获资部门绩效 | 3 | crm.page-07、crm.page-09 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：老资源激活后保留原获客事件，业绩贡献按版本比例合计 100%，重复提交不重复记功。 |
| CRM-07 | 客户管理 | `REUSE_EXISTING` | 一个家长多个孩子，同一手机号不应阻止多个孩子报名 | 3 | crm.page-07、parent-service.page-02 | 0 | 在现有 CRM 关系测试中补一条同手机号绑定两个不同孩子的回归；迁移时核对异常清单，不新建整域 E2E。 |
| STU-01 | 学员管理 | `IMPLEMENT_GAP` | 学员列表快速检索在读、停课、沉默人数 | 3 | crm.page-03、after-sales.page-01 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：固定日期夹具逐人断言三个状态及重叠矩阵，数量卡、列表和导出使用同一快照。 |
| STU-02 | 学员管理 | `IMPLEMENT_GAP` | 快速区分班课与个性化学员人数 | 3 | crm.page-03、bi-dashboard.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员同时有两条业务线权益时各线计 1、全局去重计 1，证据可下钻。 |
| STU-03 | 学员管理 | `IMPLEMENT_GAP` | 重点生源校统计分析 | 3 | crm.page-03、bi-dashboard.page-03 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：别名数据归并到同一 schoolId；学员转学按发生日匹配历史学校；漏斗逐级可回到稳定客户/学员 ID。 |
| STU-04 | 学员管理 | `IMPLEMENT_GAP` | 最近一次签约日期及最近三个月签约学员筛选 | 3 | crm.page-03 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：覆盖签约未支付、支付、部分退、全退和三个月边界，筛选结果与投影证据一致。 |
| STU-05 | 学员管理 | `EXTEND_EXISTING` | 自动生成每个学员的购课、课消、服务顾问和老师轨迹 | 3 | crm.page-03、after-sales.page-01 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：固定学员完成购课→课消→转课→退款链路，时间线顺序、来源链接、前后值和字段脱敏全部正确。 |
| PERM-01 | 权限与目标 | `IMPLEMENT_GAP` | 单校区班课与个性化由不同校长负责，客户/学员按业务隔离；同一学员可能同时属于两业务 | 2 | platform.page-03、platform.page-04 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员两业务线夹具下，班课校长只能见班课关系，主档基础信息共享但个性化事实被阻断。 |
| PERM-02 | 权限与目标 | `IMPLEMENT_GAP` | 集团层面班课与个性化数据隔离 | 2 | platform.page-04、bi-dashboard.page-10 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：集团班课角色查询/导出只包含班课，伪造个性化业务线返回 403，缓存不能跨 scopeHash 命中。 |
| PERM-03 | 权限与目标 | `IMPLEMENT_GAP` | 班课中学/小学在校区端和集团端按主管隔离 | 2 | platform.page-04 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：校区端/集团端主管分别授权小学和中学，所有列表、详情、Lookup、导出和接口伪造 ID 均按交集阻断。 |
| PERM-04 | 权限与目标 | `IMPLEMENT_GAP` | 班课/个性化老师 T 级及计算方式不同，人员不能共用一个 T 级 | 2 | hr.page-10、hr.page-15 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一教师班课/个性化不同 T 级并存；重叠区间返回 409；历史课次解析到发生日版本。 |
| PERM-05 | 权限与目标 | `EXTEND_EXISTING` | 导出限制 5–15 万，暑寒假单月扣费约 30 万；需管理员授权特定人员不限导出 | 2 | platform.page-07、security-compliance.page-05、security-compliance.page-08 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：5万/15万/30万边界、无授权、过期授权、下载次数用尽和双人审批全部覆盖，文件水印可追溯。 |
| TARGET-01 | 权限与目标 | `IMPLEMENT_GAP` | 预算区分新增、转介绍、扩科；同时设金额和人头/人次目标 | 2 | platform.page-10、bi-dashboard.page-10 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：金额、人头、人次三种单位不能混加；发布后目标行按签单类型和维度完整下钻。 |
| TARGET-02 | 权限与目标 | `IMPLEMENT_GAP` | 课消收入需排除引流、社团等课程类别 | 2 | platform.page-10、bi-dashboard.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：引流、社团类别排除；常规课程计入；冲正扣费不计，收入明细合计等于总额。 |
| TARGET-03 | 权限与目标 | `IMPLEMENT_GAP` | 预算和课消按产品线拆分，如语数英物化、1v1、高中班课 | 2 | platform.page-10、academic.page-02 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：语数英物化、1v1、高中班课映射可版本回放；跨模块 API 返回同一 productLineId。 |
| TARGET-04 | 权限与目标 | `IMPLEMENT_GAP` | 集团目标自动分解到项目/产品线并计算完成率 | 2 | platform.page-10、bi-dashboard.page-10 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：每层子目标合计等于父目标；锁定后直接修改返回 409；调整单审批后生成新版本并保留旧快照。 |
| MALL-01 | 线上商城 | `EXTEND_EXISTING` | 不支持多科连报优惠，只支持单科直减/折扣 | 4 | marketing.page-08、contract.page-01、contract.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两科/三科组合、跨学段、规则冲突和逐课程分摊合计均有固定金额断言。 |
| MALL-02 | 线上商城 | `IMPLEMENT_GAP` | 单科续费也需剩余学费结转审批后形成账户余额再报名 | 4 | marketing.page-08、contract.page-08、finance.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：单科续费结转审批后形成余额；并发支付不会超扣；驳回/撤销释放冻结余额。 |
| MALL-03 | 线上商城 | `IMPLEMENT_GAP` | 多科报名使用结转+实交时业绩归属可能错；线上合同不能备注具体业绩归属人 | 4 | marketing.page-08、contract.page-05、hr.page-15 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：结转+实交、多课程、多业绩人算例中仅认可现金进入业绩，逐项与总计差额必须为 0。 |
| DISC-01 | 优惠管理 | `EXTEND_EXISTING` | 优惠类型只有满减、直减、折扣，过于简单 | 4 | contract.page-02 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：每种新增动作至少一个命中和一个不命中用例，规则版本重放金额完全一致。 |
| DISC-02 | 优惠管理 | `IMPLEMENT_GAP` | 暑秋多科连报不能自动直减 | 4 | contract.page-02、contract.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：暑秋两科、三科及跨季节用例验证自动直减和不适用原因。 |
| DISC-03 | 优惠管理 | `EXTEND_EXISTING` | 财务设固定直减供校区自选，容易选错或凑单 | 4 | contract.page-02、contract.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：合法范围自动可选，越界直接提交被阻断；审批通过后使用审批快照而非绕过规则。 |
| DISC-04 | 优惠管理 | `REUSE_EXISTING` | 只能单选，不能“先比例折扣、再直减”等整单叠加 | 4 | contract.page-05 | 2 | 扩展现有合同关系测试，增加“比例折扣后直减”的固定算例并校验报价快照；仅在员工制单 UI 实际修改时补一个交互用例。 |
| DISC-05 | 优惠管理 | `IMPLEMENT_GAP` | 自动优惠难，校区需逐学员向财务申请配置 | 4 | contract.page-05、contract.page-06 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：合法最优组合无需审批；超范围组合自动生成审批；同输入推荐结果确定性一致。 |
| DISC-06 | 优惠管理 | `IMPLEMENT_GAP` | 退费时优惠应自动降档并扣回已课消折扣 | 4 | contract.page-02、contract.page-08 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：多科退一科、已课消、部分退款和规则版本已停用场景均按原版本重放。 |
| DISC-07 | 优惠管理 | `EXTEND_EXISTING` | 优惠多时不能按名称检索 | 4 | contract.page-02、contract.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：一万条规则夹具下名称/编号搜索正确、分页稳定，停用规则不可新选但历史 ID 可回显。 |
| CON-01 | 合同管理 | `EXTEND_EXISTING` | 合同多项优惠不能多选 | 4 | contract.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：API 提交单值字段返回 400；多选报价、合同快照和订单优惠关联完全一致。 |
| CON-02 | 合同管理 | `EXTEND_EXISTING` | 优惠多时不能筛选 | 4 | contract.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：跨页多选、输入搜索、课程变化后的重新校验及停用历史回显通过。 |
| CON-03 | 合同管理 | `IMPLEMENT_GAP` | 课程价格小数导致购买课时为小数，不是整期 | 4 | contract.page-01、contract.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：按期方案课时必须整数；5959.2/5960 和 120/120.02 用例按配置舍入且总账平衡。 |
| CON-04 | 合同管理 | `IMPLEMENT_GAP` | 转课结转时不能选择多张代金券和多个课程类型 | 4 | contract.page-08 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两张券+两份权益转三门课，分配合计、审批和执行后来源链无丢失。 |
| CON-05 | 合同管理 | `IMPLEMENT_GAP` | 跨校区录合同后，校区无法关联跨校合同 | 4 | contract.page-04、contract.page-08 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：A 校合同在 B 校按授权可见，B 校不能修改金额；授权过期/撤销后立即失效并保留审计。 |
| CON-06 | 合同管理 | `IMPLEMENT_GAP` | 结课停课三个月后再报名，按老生召回政策应算新增 | 4 | contract.page-05、contract.page-08 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：结课/停课未满与超过三个月边界、期间课消、退款后召回均有判定用例。 |
| CON-07 | 合同管理 | `IMPLEMENT_GAP` | 多科收费按比例把每课程分给多人，实际通常每课程归一个业绩人 | 4 | contract.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：普通课程项只能一个主业绩人；多人比例合计 100% 且无审批不能盖章。 |
| CON-08 | 合同管理 | `IMPLEMENT_GAP` | 同合同多科导致同课程出现三名提成人，财务需手工修正 | 4 | contract.page-04、contract.page-09、hr.page-15 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：重复三人提成夹具被唯一约束/冲突扫描识别，修正后业绩和工资差异使用同一 correctionId。 |
| COURSE-01 | 课程设置 | `EXTEND_EXISTING` | 班课、1v1、1vN分别设置导致同类型不同名称和查询多选 | 2 | academic.page-02 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：班课/1v1/1vN 标准类型不因显示名产生多条枚举，历史别名迁移可复核。 |
| COURSE-02 | 课程设置 | `IMPLEMENT_GAP` | 课程总价与课时精度造成 5959.2/5960、120/120.02 | 2 | academic.page-02、contract.page-01 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：5959.2/5960、120/120.02 和多次课消累计尾差算例在报价、课消、财务一致。 |
| PARAM-01 | 常用参数 | `IMPLEMENT_GAP` | 自定义字段单选/多选值不能停用，只能删除/修改，影响历史 | 2 | platform.page-09 | 4 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：停用值不能新选、历史记录继续显示原标签，删除被引用选项返回 409。 |
| APPROVAL-01 | 员工与审批 | `REUSE_EXISTING` | 审批节点人员变动需重配整个流程 | 2 | platform.page-05、hr.page-19 | 2 | 扩展现有审批服务集成测试，覆盖离职员工有待办时的影响扫描、转交和异常池；不新建审批域 Playwright 套件。 |
| APPROVAL-02 | 员工与审批 | `IMPLEMENT_GAP` | 驳回重提只能从头；需可选当前节点或从头 | 2 | platform.page-05 | 2 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：三种模式分别验证节点执行序列；模板禁用模式返回 400；已完成实例不能重提。 |
| APPROVAL-03 | 员工与审批 | `IMPLEMENT_GAP` | 全职/兼职切换需记录生效日期，便于人效统计 | 2 | hr.page-02、hr.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：全职转兼职前后课次分别匹配正确身份，重叠生效期被阻断，历史工资不被当前身份改写。 |
| APPROVAL-04 | 员工与审批 | `REUSE_EXISTING` | 结转审批提交前校验目标班容量，避免审批时报满员 | 2 | contract.page-08、academic.page-04 | 2 | 复用 AfterSalesRelationClosureTest；只有新增容量变化提示时补一个响应断言，并保留现有执行防超员覆盖，不新建独立 E2E。 |
| ASSET-01 | 物品管理 | `IMPLEMENT_GAP` | 增加物品导出 | 5 | asset.page-02、asset.page-04、asset.page-05、asset.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：五类导出字段、总行数、数量和成本与页面同条件汇总一致，越权仓库数据不进入文件。 |
| ASSET-02 | 物品管理 | `UAT_FIRST` | 物品类型栏缺滚动条，左右页面应分割 | 5 | asset.page-02 | 0 | 先导入真实类别数据做三视口 UAT；布局失败后补一条聚焦滚动区域的 Playwright，用例通过后不再增加 JUnit/Vitest。 |
| PAY-01 | 教师课时费 | `IMPLEMENT_GAP` | 班课课时费按教师 T 级、校区班型和当次扣费人数计算 | 5 | hr.page-15、hr.page-17 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：班课固定课次分别用 0/1/满班扣费人数验证规则，冲正学员不计，金额逐行可重放。 |
| PAY-02 | 教师课时费 | `IMPLEMENT_GAP` | 个性化课时费按 T 级、年级、班型和实际课时计算 | 5 | hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不同年级、1v1/1vN、特殊系数和小数课时算例按精度规则计算并与汇总一致。 |
| PAY-03 | 教师课时费 | `IMPLEMENT_GAP` | 班课与个性化教师 T 级必须分别维护 | 5 | hr.page-10、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一教师两套 T 级同时存在且互不覆盖，历史课次解析发生日版本。 |
| PAY-04 | 教师课时费 | `IMPLEMENT_GAP` | 当前系统同一老师只能设一个 T 级，无法支撑两套课酬 | 5 | hr.page-10 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：导入重复、重叠生效期、未知教师/业务线和合法多 T 级记录均有行级结果。 |
| PAY-05 | 教师课时费 | `IMPLEMENT_GAP` | 枣庄校区存在单独的课时费计算政策 | 5 | hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：枣庄命中校区规则，其他校区命中区域/集团规则；历史日期解析旧版本。 |
| PAY-06 | 教师课时费 | `IMPLEMENT_GAP` | 兼职教师有独立课酬标准 | 5 | hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：兼职转全职前后课次使用各自标准，修改当前身份不改变历史计算。 |
| PAY-07 | 教师课时费 | `IMPLEMENT_GAP` | 书法、迪聪、美术等课程按实际扣费人头计算 | 5 | academic.page-02、hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：成功、失败、冲正和重复扣费事实中只计唯一成功未冲正学员。 |
| PAY-08 | 教师课时费 | `IMPLEMENT_GAP` | 活动课按活动方案或特殊标准计算课时费 | 5 | marketing.page-03、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一活动主讲/助教不同标准、活动取消和核定数量调整均有明确结果。 |
| PAY-09 | 教师课时费 | `IMPLEMENT_GAP` | 跨月补录、系统锁定后调整会导致课时费漏算或错月 | 5 | hr.page-15、hr.page-17、hr.page-18 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨月补录和锁定后调整进入下一期正负追补，已发布工资条金额不被覆盖。 |
| PAY-10 | 教师课时费 | `IMPLEMENT_GAP` | 常规课程需按统一常规课标准计算 | 5 | hr.page-15、hr.page-21 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：常规课命中默认规则；删除覆盖规则后结算失败并进入异常池，补规则重试成功。 |
| PAY-11 | 教师课时费 | `IMPLEMENT_GAP` | 小学与中学混合/不同学段的班级课时费口径不同 | 5 | academic.page-04、hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：小学、中学和混合班三种场景按唯一已审批口径计算，未配置混合口径阻断结算。 |
| TRANS-01 | 过渡奖励 | `IMPLEMENT_GAP` | 不同校区/学段/课程的过渡奖励标准不同，当前需人工判断 | 5 | hr.page-15、hr.page-16 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不同校区/学段/课程固定算例命中唯一规则，重叠规则发布被阻断。 |
| TRANS-02 | 过渡奖励 | `EXTEND_EXISTING` | 历史奖励记录分散，难以追溯某学员曾给谁、给多少 | 5 | hr.page-15、hr.page-18 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员多合同多奖励人记录可按稳定 ID 查询，明细合计等于主表金额。 |
| TRANS-03 | 过渡奖励 | `EXTEND_EXISTING` | 奖励金额需跨客户、合同、课消、退款等模块核对 | 5 | hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：重复事件不重复奖励；相同快照和规则版本重放结果字节级一致。 |
| TRANS-04 | 过渡奖励 | `IMPLEMENT_GAP` | 新签、续费、扩科、转介绍、召回等签单类型的奖励规则不同 | 5 | contract.page-05、hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五类签单类型各一套奖励算例，未经审批的人工改类不影响奖励。 |
| TRANS-05 | 过渡奖励 | `IMPLEMENT_GAP` | 奖励基数、人数/金额口径及签单类型判断不统一 | 5 | hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：实收、净收、合同额、人头、人次及定金/结转/退款组合均有固定基数断言。 |
| TRANS-06 | 过渡奖励 | `EXTEND_EXISTING` | 需要在统一学员视图查看过渡奖励 | 5 | crm.page-03、hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：Student 360 汇总等于奖励台账同条件合计，字段权限隐藏其他员工敏感薪酬。 |
| TRANS-07 | 过渡奖励 | `IMPLEMENT_GAP` | 系统自动给符合条件的学员打标签并生成奖励基数 | 5 | crm.page-03、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：符合条件自动生成一次候选和系统标签；人工编辑系统标签返回 403。 |
| TRANS-08 | 过渡奖励 | `EXTEND_EXISTING` | 退款后需改变续费判断并追溯过渡奖励 | 5 | hr.page-15、hr.page-17 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：冻结中退款直接冲减，已发放退款生成负向工资差异，重复退款事件幂等。 |
| TRANS-09 | 过渡奖励 | `IMPLEMENT_GAP` | 奖励需冻结 31 天后再确认，期间发生退款要扣除 | 5 | hr.page-15、hr.page-18 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：31 天边界前不能确认，到期可确认，冻结期间退款进入追回；非法状态迁移返回 409。 |
| TRANS-10 | 过渡奖励 | `IMPLEMENT_GAP` | 奖励规则需按校区、年级、课程等维度配置 | 5 | hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：类型错误、空范围、冲突规则和合法规则分别断言 400/409/成功。 |
| TRANS-11 | 过渡奖励 | `IMPLEMENT_GAP` | 需要独立计算页、明细查询和导出 | 5 | hr.page-15、hr.page-20 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五队列互斥/覆盖规则明确，导出行与查询快照一致且包含来源 ID 和规则版本。 |
| TRANS-12 | 过渡奖励 | `IMPLEMENT_GAP` | 合同、课消、退款变化后应自动同步，不应人工搬数 | 5 | system-ops.page-04、system-ops.page-05、hr.page-21 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：模拟消费失败进入重试/死信；对账识别漏事件并修复；同 eventId 只生成一份奖励结果。 |
| TRANS-13 | 过渡奖励 | `EXTEND_EXISTING` | 需要“核算—员工确认—异议—修正—终审”闭环 | 5 | hr.page-15、hr.page-18、hr.page-19 | 4 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：员工确认和异议互斥，修正保留原金额，终审后只发布差异版本。 |
| TRANS-14 | 过渡奖励 | `EXTEND_EXISTING` | 学员转校、转班、转课等轨迹要参与奖励判定 | 5 | crm.page-03、hr.page-15 | 2 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：转校前后两笔合同分别使用当时校区和课程关系，当前主档变化不改历史结果。 |
| TRANS-15 | 过渡奖励 | `IMPLEMENT_GAP` | 系统应自动判断合同属于新签、续费、扩科、转介绍或召回 | 5 | contract.page-05、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：五种类型、边界优先级和审批纠错均有固定历史数据回放；合同与奖励读取相同结果。 |
| REFUND-01 | 退费扣款 | `IMPLEMENT_GAP` | 同一学员多合同退费需逐笔处理，缺少批量核算 | 5 | after-sales.page-08、finance.page-05、contract.page-08 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员三份合同夹具中两笔成功、一笔失败时批次为 PARTIAL_SUCCESS；成功流水只生成一次，失败项修复后可幂等重试。 |
| REFUND-02 | 退费扣款 | `IMPLEMENT_GAP` | 财务需在一个页面看学员全部合同、余额、课消、优惠和退款 | 5 | after-sales.page-01、crm.page-03、finance.page-05 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨三份合同、含余额结转和部分退款的数据中，各区块来源 ID 可打开，余额恒等式成立且总差额为 0。 |
| REFUND-03 | 退费扣款 | `EXTEND_EXISTING` | 转校/转课/课消/退款明细需连续可追溯 | 5 | after-sales.page-08、crm.page-03 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：完成转校→转课→课消→冲正→退款链路，页面和导出顺序、前后值、单据 ID、操作人完全一致。 |
| REFUND-04 | 退费扣款 | `IMPLEMENT_GAP` | 退款后多科优惠应降档并扣回已享优惠 | 5 | after-sales.page-08、contract.page-02 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：三科联报退一科、阶梯优惠降档、规则已下线三类算例均按原版本重放，分摊差额合计等于扣回额。 |
| REFUND-05 | 退费扣款 | `IMPLEMENT_GAP` | 不同事业部、岗位和责任情形采用不同扣款规则 | 5 | after-sales.page-08、hr.page-19 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨事业部、不同岗位、多人分责和规则生效日边界均命中唯一版本；未审批认定不能生成扣款。 |
| REFUND-06 | 退费扣款 | `EXTEND_EXISTING` | 退费时需要看到签单、服务、教师、转课等完整上下文 | 5 | after-sales.page-08、finance.page-05 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：跨校转课且更换服务顾问/教师的退款，快照能还原当时关系；主档后改不影响已提交退款。 |
| REFUND-07 | 退费扣款 | `EXTEND_EXISTING` | 退费应自动追回已发放过渡奖励 | 5 | after-sales.page-08、hr.page-15、hr.page-17 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：冻结中、已确认未发、已发放三种奖励分别冲减/追回；重复退款事件不重复生成工资差异。 |
| REFUND-08 | 退费扣款 | `IMPLEMENT_GAP` | 退费应按规则自动计算教师扣款 | 5 | after-sales.page-08、hr.page-15、hr.page-18 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：不可扣、按比例、触发上限和已结课酬四类算例正确；申诉通过生成反向差异且不修改原扣款记录。 |
| PERF-01 | 业绩核算 | `EXTEND_EXISTING` | 不同区域、岗位采用不同业绩标准 | 5 | hr.page-12、hr.page-15、bi-dashboard.page-04 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一员工跨区域调岗前后业务按发生日匹配不同标准；冲突规则不能发布，历史结算不随当前岗位变化。 |
| PERF-02 | 业绩核算 | `IMPLEMENT_GAP` | 人头应去重，扩科/多科的人次与科次需另算 | 5 | hr.page-12、bi-dashboard.page-04 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同一学员扩两科时人头 1、人次按约定事件、科次 2；跨周期去重重新开始，逐笔合计等于汇总。 |
| PERF-03 | 业绩核算 | `IMPLEMENT_GAP` | 业绩按实际现金计算：定金如何计入、结转余额不计实交 | 5 | finance.page-05、contract.page-05、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：现金、定金、余额、结转和退款组合中只认可规定来源；定金只在政策指定时点记一次，退款形成负向贡献。 |
| PERF-04 | 业绩核算 | `IMPLEMENT_GAP` | 多课程、多业绩人时应按课程逐项归属 | 5 | contract.page-05、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：三课程、四业绩人组合逐项合计为认可实收；缺主业绩人、比例超 100% 和跨权限员工均被阻断。 |
| PERF-05 | 业绩核算 | `EXTEND_EXISTING` | 常规课程与活动课程采用不同业绩标准 | 5 | marketing.page-03、contract.page-05、hr.page-15 | 3 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：同课程在常规和活动两笔合同分别命中对应规则；活动过期后不再命中，汇总可分别对账。 |
| PERF-06 | 业绩核算 | `IMPLEMENT_GAP` | 需形成覆盖签单类型、课程、岗位、区域、退款的完整规则矩阵 | 5 | hr.page-12、hr.page-15、hr.page-18、hr.page-21 | 4 | 后端规则/PostgreSQL 集成测试加历史期间对账；仅作为五条跨模块主链之一进入 E2E。验收场景：历史月份逐笔回放后明细合计、账单、BI 和工资台账一致；关账后只允许差异追补，重复退款不重复冲减。 |
| ACTIVE-01 | 在读学员 | `IMPLEMENT_GAP` | 在读口径应为统计期间有有效课消的学员，按学员去重，并限定/排除课程类型 | 6 | bi-dashboard.page-05、bi-dashboard.page-10 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：同一学员多课次只计 1；冲正、引流课和排除分类不计；跨校稳定 ID 按规则去重，逐笔证据可复算。 |
| ACTIVE-02 | 在读学员 | `EXTEND_EXISTING` | 当前需手工统计，希望系统自动计算、查看明细和导出 | 6 | bi-dashboard.page-05、bi-dashboard.page-11、bi-dashboard.page-12 | 3 | 查询/口径集成测试加固定数据集对账；页面交互变化时补共享前端契约，E2E 归入“指标、报表与导出”主链。验收场景：连续一个结算周期自动生成快照和对账；导出行数/学员 ID 与下钻一致，未处理差异阻断旧口径下线。 |
| HRF-01 | 人力反馈补充 | `IMPLEMENT_GAP` | 需要把现用系统 2 的工资/人员数据迁移到新平台 | 6 | hr.page-02、hr.page-17、finance.page-19 | 4 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：全量和增量各演练一次；员工数、工资批次数、应发/扣减/实发金额差异为 0 后才能双签切换。 |
| HRF-02 | 人力反馈补充 | `IMPLEMENT_GAP` | 退款后收入、业绩和工资应同步扣回 | 6 | finance.page-11、hr.page-17、hr.page-21 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：退款后同一 ID 在三台账均可查，金额按规则勾稽；模拟一个消费者失败时其余已落账且修复后差异归零。 |
| HRF-03 | 人力反馈补充 | `IMPLEMENT_GAP` | 需要查看教师当前负荷和未来排课预测 | 6 | academic.page-05、hr.page-01、hr.page-21 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未来 4/8/12 周预测均可复算；请假和新排课触发增量更新，超载/缺口阈值预警与课表明细一致。 |
| LOAD-01 | 数据加载策略 | `IMPLEMENT_GAP` | 录入客户、排课、考勤、收款等日期型列表默认只加载近一周或当月至今 | 1 | crm.page-03、academic.page-05、academic.page-11、finance.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：四类资源首屏只请求配置周期且有分页；跨最大同步天数返回 422 并引导异步查询。 |
| LOAD-02 | 数据加载策略 | `IMPLEMENT_GAP` | 学员、已报课程、合同、班级、1v1 学员等大列表打开时不加载，查询后再加载 | 1 | crm.page-03、contract.page-04、academic.page-04、academic.page-01 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：Playwright 监听首屏无事实接口；空值/空数组不算有效条件，满足任一条件组后只发一次查询，清空后恢复空态。 |
| LOAD-03 | 数据加载策略 | `IMPLEMENT_GAP` | 课消、考勤、客户分析等时间型报表默认按近一周加载 | 1 | bi-dashboard.page-03、bi-dashboard.page-05、bi-dashboard.page-06 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：上海时区周跨月、月末和夏令时无关边界正确；三类报表首查均为连续 7 个自然日。 |
| LOAD-04 | 数据加载策略 | `IMPLEMENT_GAP` | 业绩、转化率、带生量、续班率、退费率等高聚合报表默认不加载 | 1 | bi-dashboard.page-04、bi-dashboard.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：五类报表首屏零事实请求；缺周期/组织返回 422；重复提交命中同权限缓存，取消后不再发布结果。 |
| LOAD-05 | 数据加载策略 | `IMPLEMENT_GAP` | 报表应先选按校区/科目等汇总方式，再点查询；切换汇总方式不要自动刷新 | 1 | bi-dashboard.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：连续切换校区/科目/汇总方式不产生网络请求；点击查询仅提交最后草稿，刷新后可恢复上次已提交条件。 |
| QUERY-01 | 数据查询提效 | `EXTEND_EXISTING` | 客户、学员、排课、考勤等常见列表查询响应慢 | 1 | crm.page-03、academic.page-05、academic.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：百万级脱敏夹具执行固定场景，常用列表 P95 达项目门槛；计划哈希稳定且分页无重复/漏行。 |
| QUERY-02 | 数据查询提效 | `IMPLEMENT_GAP` | 报表日期选择增加周、月、季、年快捷项 | 1 | bi-dashboard.page-03、bi-dashboard.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：周跨月、季度跨年、闰年、上期和去年同期边界快照固定；所有报表提交同一闭开区间合同。 |
| QUERY-03 | 数据查询提效 | `EXTEND_EXISTING` | 三个月以上报表查询和导出需提速 | 1 | bi-dashboard.page-11、platform.page-07 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：90/91/365 天边界选择正确执行路径；导出不新增事实 SQL，查询与导出行数、合计及口径版本一致。 |
| LIST-01 | 列表数据与汇总 | `EXTEND_EXISTING` | 校区、档期、学员状态等重要查询字段支持多选 | 1 | crm.page-03、contract.page-04、academic.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未传、空数组和全枚举结果一致，部分选择准确；201 个值返回 422，伪造越权校区返回 403。 |
| LIST-02 | 列表数据与汇总 | `EXTEND_EXISTING` | 列表按当前查询条件展示总数、合计金额等总汇总 | 1 | contract.page-04、finance.page-05、academic.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：多页数据中当前页合计与总汇总不同且总汇总正确；列表、独立汇总和导出统计使用相同 filterHash/scopeHash。 |
| LIST-03 | 列表数据与汇总 | `EXTEND_EXISTING` | 报表按当前查询条件显示总汇总，无需导出手算 | 1 | bi-dashboard.page-05、bi-dashboard.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：金额可加指标的小计之和等于总计；转化率按总分子/总分母重算而非平均，页面与导出总计一致。 |
| ID-01 | 重要数据 ID | `IMPLEMENT_GAP` | 考勤列表、扣费记录和导出需显示员工/教师 ID | 1 | academic.page-11、hr.page-08、platform.page-07 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名教师夹具能按 UUID 区分；页面/API/导出三端 ID、编号、姓名逐行一致，缺 ID 的历史数据进入异常清单。 |
| ID-02 | 重要数据 ID | `REUSE_EXISTING` | 客户 ID 当前无问题，应在新平台继续保留 | 1 | crm.page-03、finance.page-19 | 0 | 复用稳定 UUID 和客户关系现有测试；历史迁移发生时增加一条新旧 ID 一对一映射及异常清单对账，不新增独立 E2E。 |
| ID-03 | 重要数据 ID | `EXTEND_EXISTING` | 课消分析只有学员姓名、无学员 ID，无法唯一匹配 | 1 | bi-dashboard.page-05、academic.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两个同名学员的分析和导出能以 UUID 唯一匹配，逐笔课消回到正确权益和家庭，汇总不串档。 |
| ID-04 | 重要数据 ID | `EXTEND_EXISTING` | 在班学员、排课、班课考勤、扣费等页面需课程 ID | 1 | academic.page-04、academic.page-05、academic.page-11 | 4 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：四类 API、页面列和导出均有相同三字段；重名课程用 ID 区分，契约快照防止字段丢失。 |
| ID-05 | 重要数据 ID | `EXTEND_EXISTING` | 在班学员、排课、班课考勤、扣费等页面需班级 ID | 1 | academic.page-04、academic.page-05、academic.page-11 | 4 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名班级夹具在四类查询和导出均按 UUID 区分，页面跳转使用 classId，名称修改不破坏历史关系。 |
| ID-06 | 重要数据 ID | `IMPLEMENT_GAP` | 在班学员需显示其实际使用的合同 ID | 1 | academic.page-04、contract.page-04 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：报班→合同项→权益→扣费链 ID 完整；转班后保留原权益来源，缺失映射不能静默显示错误合同。 |
| ID-07 | 重要数据 ID | `IMPLEMENT_GAP` | 在班学员需显示已报读课程 ID，便于快速检索 | 1 | academic.page-04、academic.page-02 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同一学员多个同名课程权益可按已报课程 ID 准确检索，页面/API/导出来源链一致。 |
| ID-08 | 重要数据 ID | `IMPLEMENT_GAP` | 教室管理导出需教室 ID，校区内可能重名 | 1 | academic.page-03、platform.page-07 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：两个校区同名、同校区同名和编码冲突分别符合规则；导出用 UUID+校区唯一定位教室。 |
| ID-09 | 重要数据 ID | `IMPLEMENT_GAP` | 1vN 考勤列表需小组/小组班 ID，不能只有名称 | 1 | academic.page-04、academic.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同名小组按 UUID 区分，1vN 考勤页面/API/导出均有三字段；改名后历史记录仍指向同一小组。 |
| OPT-01 | 优化建议 | `IMPLEMENT_GAP` | 学员与客户可批量修改年级、性别、公立学校 | 7 | crm.page-03、platform.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：200 条含合法、越权、无效学校和并发变更数据逐条返回结果；成功项有审计，失败重试不重复成功项。 |
| OPT-02 | 优化建议 | `IMPLEMENT_GAP` | 扣费产生时快照学员年级、教师全兼职、班级/课程/教师/合同 ID，并在导出增加课程分类 | 7 | academic.page-11、bi-dashboard.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：扣费后修改年级、教师身份、课程分类和名称，历史 API/导出仍返回发生时值；缺快照事实被阻断或列入修复。 |
| OPT-03 | 优化建议 | `EXTEND_EXISTING` | 在班学员按班级状态筛选已结课/上课中/未开课 | 7 | academic.page-04 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：未开课、上课中、已结课和组合筛选四组结果正确；未传/空数组语义一致，投影与班级详情状态相同。 |
| OPT-04 | 优化建议 | `IMPLEMENT_GAP` | 班级管理增加“该班在指定日期内有排课”的上课日期筛选 | 7 | academic.page-01、academic.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：有课、只有已取消课、跨日课和边界课次四类班级结果正确；EXPLAIN 使用目标索引且分页班级不重复。 |
| OPT-05 | 优化建议 | `IMPLEMENT_GAP` | 员工管理导出增加员工 ID，主要用于教师匹配 | 7 | hr.page-02、platform.page-07 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：员工、考勤、扣费、课酬、工资五类导出首个身份字段均为 employeeId；同名教师匹配不串人。 |
| OPT-06 | 优化建议 | `EXTEND_EXISTING` | 课程有效/无效支持多选；导入和手工新增时课程名称唯一 | 7 | academic.page-02、platform.page-07 | 4 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：单状态、多状态、空状态语义正确；手工和导入对同名同范围均阻断，跨业务线合法，合并保留全部历史 ID 映射。 |
| OPT-07 | 优化建议 | `IMPLEMENT_GAP` | 每校区每课程价格不同，需要可导出的价格明细 | 7 | contract.page-01、platform.page-07 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：同课程多校区不同价逐行正确；总价÷课时与单价按精度规则对账，页面筛选与导出 filterHash 一致。 |
| BUG-01 | 已发现缺陷 | `IMPLEMENT_GAP` | 客户跟进状态全选与全不选结果不一致；不勾选会漏已成交/已试听 | 7 | crm.page-04 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：不传、null、空数组、全选四组返回相同全集并包含已成交/已试听，部分选只返回目标状态；导出一致。 |
| BUG-02 | 已发现缺陷 | `EXTEND_EXISTING` | 学员收费优惠/优惠券太多，缺少搜索且只能单选 | 7 | contract.page-02、contract.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：一万条规则数据下远程搜索 P95 达门槛；多选叠加/冲突解释正确，任何入口不再发送单值字段。 |
| BUG-03 | 已发现缺陷 | `UAT_FIRST` | 学员列表“录入日期/入学日期、班级/班别”在查询与导出名称不一致 | 7 | crm.page-03、platform.page-07 | 0 | 先做一次同数据集页面/导出 UAT；失败修复后增加一条共享字段标签契约测试，不新建 JUnit、Vitest、Playwright 三套测试。 |
| BUG-04 | 已发现缺陷 | `IMPLEMENT_GAP` | 排课次数不足时删排课，班级结课日期不变化；应去掉限制并重算 | 7 | academic.page-01、academic.page-05 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：删除首/中/末课次和全部课次均可执行并正确重算；手工锁定不被覆盖但差异有审计和提示。 |
| BUG-05 | 已发现缺陷 | `IMPLEMENT_GAP` | 学员出班后历史月份“应到人数”随当前名单变化，数据未锁定 | 7 | academic.page-11 | 3 | 按实际改动选择一层主测试：后端规则用 JUnit/PostgreSQL，前端适配用 Vitest，关键用户路径才用 Playwright；验收场景：历史月份学员出班前后应到人数不变；未来课次名单更新，历史修正保留旧版本且报表引用指定版本。 |
| BUG-06 | 已发现缺陷 | `UAT_FIRST` | 扣费页面“课程类型/扣费来源”与导出“上课类型/来源”命名不一致 | 7 | academic.page-11、platform.page-07 | 0 | 先做页面/API/导出同数据集 UAT；失败修复后与 BUG-03 共用一条字段标签契约测试，不增加独立 E2E。 |

## 5. 完成判定

本计划是 132 项业务结果的全量交付合同，不是要求所有项产生相同技术产物。完成时必须同时满足：

- 132 个原始编号全部且仅出现一次，23 个问题域全部完成差异确认、必要实现和逐项 UAT；
- 297 个接口引用均有复用、扩展、候选新增或不适用结论；只有实际新增 operation 才要求 Controller/Models/OpenAPI/前端 client 闭环；
- 实际创建的 Flyway 在空库和适用的 V89 升级路径成功；没有 Schema 改动的需求不要求迁移证据；
- 受影响测试在开发阶段通过；最终发布候选只运行一次全量后端、前端、构建、既有发布门禁和五条共享 E2E；
- 历史月回放、财务/业绩/课酬/奖励/退款/工资/BI 对账无未解释差异；
- 132 项均有独立 UAT 签字和可追溯证据；REUSE_EXISTING/UAT_FIRST 允许零代码，进度台账全部为 `uat_passed`；
- 发布候选已合并开工后新增的最新主分支代码，并在合并后重新通过全部门禁。

