122 lines
9.7 KiB
Markdown
122 lines
9.7 KiB
Markdown
# Legacy Analytics V2 Contract Audit
|
|||
|
|
|
||
|
|
## 1. Scope and evidence
|
||
|
|
|
||
|
|
- Branch: `fix/legacy-analytics-v2-contract`
|
||
|
|
- Base: `d162954cd1e26c6948408a8304ca333ba9fe7a40`
|
||
|
|
- Scope: `/train/report`, `/train/checkin`, `/train/duration`, `/train/portrait`
|
||
|
|
- Sources, in precedence order: frozen API/PRD contract, V2 migration DDL, read-only live Kingbase metadata, current V2 domain/service implementation, legacy generator code.
|
||
|
|
- Live database: KingbaseES V009R003C018, `database_mode=mysql`; all four audited tables exist and contain zero rows.
|
||
|
|
- The live check queried only metadata, constraints, indexes, counts, and non-sensitive enum distributions. No DDL or DML was executed.
|
||
|
|
|
||
|
|
## 2. End-to-end fact chains
|
||
|
|
|
||
|
|
### 2.1 Report
|
||
|
|
|
||
|
|
| Layer | Current fact |
|
||
|
|
|---|---|
|
||
|
|
| Vue | `build_ui/src/views/train/report/index.vue`; generator CRUD showing raw plan/student/resource IDs |
|
||
|
|
| Frontend API | `build_ui/src/api/train/report.js`; `/train/report/list`, `/{id}`, POST, PUT, DELETE |
|
||
|
|
| Controller | `TrainClassReportController`; generator CRUD |
|
||
|
|
| Permission | Backend `system:report:*`, frontend `train:report:*`; inconsistent |
|
||
|
|
| Service | `ITrainClassReportService` / `TrainClassReportServiceImpl`; pass-through CRUD |
|
||
|
|
| Mapper | `TrainClassReportMapper.xml` |
|
||
|
|
| Domain | `TrainClassReport` |
|
||
|
|
| V2 table | `train_class_report` |
|
||
|
|
| Live columns | `report_id, enrollment_id, report_way, report_device_sn, report_time, verify_result, verify_note, create_by, create_time, remark` |
|
||
|
|
| Compatibility | **INCOMPATIBLE**: legacy mapper references `plan_id, student_id, report_status, alloc_dorm_id, alloc_classroom_id, alloc_site_id`, none of which exists |
|
||
|
|
| Runtime capability before fix | READ/CREATE/UPDATE fail at SQL level; DELETE could execute by ID but is not a valid report lifecycle operation |
|
||
|
|
| Formal contract | Confirm via `POST /train/enrollments/{enrollmentId}/report`; plan report list is P1. Report belongs to phase 1, but generic table CRUD does not. |
|
||
|
|
| Disposition | **KEEP_READ_ONLY** for this legacy list; future report confirmation remains a dedicated transactional implementation |
|
||
|
|
|
||
|
|
`report_status` and `verify_result` are different concepts. Enrollment status owns not-reported/reported/leave/abandoned lifecycle; `verify_result` records identity verification outcome. The legacy field must be retired, not renamed. Accommodation comes from `train_accommodation`; classroom/site come from `train_lesson` scheduling relationships and must not be stored on report.
|
||
|
|
|
||
|
|
### 2.2 Checkin
|
||
|
|
|
||
|
|
| Layer | Current fact |
|
||
|
|
|---|---|
|
||
|
|
| Vue | `build_ui/src/views/train/checkin/index.vue`; raw generator CRUD |
|
||
|
|
| Frontend API | `build_ui/src/api/train/checkin.js`; `/train/checkin/list`, `/{id}`, POST, PUT, DELETE |
|
||
|
|
| Controller | `TrainStudentCheckinController`; generator CRUD |
|
||
|
|
| Permission | Backend `system:checkin:*`, frontend `train:checkin:*`; inconsistent with formal `train:attendance:*` |
|
||
|
|
| Service | `ITrainStudentCheckinService` / implementation; pass-through CRUD |
|
||
|
|
| Mapper | `TrainStudentCheckinMapper.xml` |
|
||
|
|
| Domain | `TrainStudentCheckin` |
|
||
|
|
| V2 table | `train_student_checkin` |
|
||
|
|
| Live columns | `checkin_id, enrollment_id, lesson_id, board_id, checkin_way, sign_in_time, sign_out_time, attendance_status, manual_flag` plus audit columns |
|
||
|
|
| Compatibility | **INCOMPATIBLE**: legacy mapper references `plan_id, c_id, student_id, checkin_time, checkin_status, lesson_start_time, lesson_end_time` |
|
||
|
|
| Runtime capability before fix | READ/CREATE/UPDATE fail; DELETE is SQL-capable but violates the attendance audit model |
|
||
|
|
| Formal contract | V2 attendance is directly bound to `enrollment_id + lesson_id`; sign-in, sign-out and manual adjustment are dedicated APIs. It is phase-1 business. |
|
||
|
|
| Disposition | **KEEP_READ_ONLY** for the legacy global list; formal attendance writes remain dedicated business actions |
|
||
|
|
|
||
|
|
The live V2 model proves that `lesson_id` is already the formal relationship. `attendance_status` is the stored final attendance result; it is not the separate legacy `checkin_status` field and must use values `0 absent, 1 normal, 2 late, 3 early leave, 4 leave`. `checkin_way` uses `1 face, 2 card, 3 QR, 4 teacher/manual`.
|
||
|
|
|
||
|
|
### 2.3 Duration
|
||
|
|
|
||
|
|
| Layer | Current fact |
|
||
|
|
|---|---|
|
||
|
|
| Vue | `build_ui/src/views/train/duration/index.vue`; generator CRUD |
|
||
|
|
| Frontend API | `build_ui/src/api/train/duration.js`; `/train/duration/list`, `/{id}`, POST, PUT, DELETE |
|
||
|
|
| Controller | `TrainStudyDurationController`; generator CRUD |
|
||
|
|
| Permission | Backend `system:duration:*`, frontend `train:duration:*`; inconsistent with formal `train:learning:*` |
|
||
|
|
| Service | `ITrainStudyDurationService` / implementation; pass-through CRUD |
|
||
|
|
| Mapper | `TrainStudyDurationMapper.xml` |
|
||
|
|
| Domain | `TrainStudyDuration` |
|
||
|
|
| V2 table | `train_study_duration` |
|
||
|
|
| Live columns | `dur_id, enrollment_id, plan_course_id, resource_id, learning_content, source_system, source_event_id, start_time, end_time, study_seconds, sync_time` plus audit columns |
|
||
|
|
| Compatibility | **INCOMPATIBLE**: legacy mapper references `student_id, plan_id, c_id, device_type` |
|
||
|
|
| Runtime capability before fix | READ/CREATE/UPDATE fail; DELETE is SQL-capable but invalid for immutable source events |
|
||
|
|
| Formal contract | P0 write path is device/system event ingestion; P1 list/detail is read-only. Phase-1 business. |
|
||
|
|
| Disposition | **KEEP_READ_ONLY** |
|
||
|
|
|
||
|
|
The old `device_type` label set describes source devices, but V2 deliberately models this as `source_system` plus content/resource references. No replacement column is added.
|
||
|
|
|
||
|
|
### 2.4 Portrait
|
||
|
|
|
||
|
|
| Layer | Current fact |
|
||
|
|
|---|---|
|
||
|
|
| Vue | `build_ui/src/views/train/portrait/index.vue`; generator CRUD |
|
||
|
|
| Frontend API | `build_ui/src/api/train/portrait.js`; `/train/portrait/list`, `/{id}`, POST, PUT, DELETE |
|
||
|
|
| Controller | `TrainStudentPortraitController`; generator CRUD |
|
||
|
|
| Permission | Backend `system:portrait:*`, frontend `train:portrait:*`; inconsistent |
|
||
|
|
| Service | `ITrainStudentPortraitService` / implementation; pass-through CRUD |
|
||
|
|
| Mapper | `TrainStudentPortraitMapper.xml` |
|
||
|
|
| Domain | `TrainStudentPortrait` |
|
||
|
|
| V2 table | `train_student_portrait` |
|
||
|
|
| Live columns | `portrait_id, enrollment_id, attendance_rate, theory_score, practical_score, total_study_hour, label_tag, portrait_content, generated_time` plus audit columns |
|
||
|
|
| Compatibility | **INCOMPATIBLE**: legacy mapper references `student_id, plan_id, avg_score, checkin_rate`; it also omits V2 theory/practical/generated fields |
|
||
|
|
| Runtime capability before fix | READ/CREATE/UPDATE fail; DELETE is SQL-capable but invalid for generated analytics |
|
||
|
|
| Formal contract | P1 generation/refresh is a system calculation; queries are read-only. Phase-1 business. |
|
||
|
|
| Disposition | **KEEP_READ_ONLY** |
|
||
|
|
|
||
|
|
## 3. CRUD reasonableness matrix
|
||
|
|
|
||
|
|
| Module | CREATE | READ | UPDATE | DELETE |
|
||
|
|
|---|---|---|---|---|
|
||
|
|
| Report | **REQUIRED**, but only through dedicated report-confirm business action | **REQUIRED** | **ADMIN_ONLY**, only through an audited correction workflow not generic CRUD | **NOT_REQUIRED** |
|
||
|
|
| Checkin | **DEVICE_GENERATED** or dedicated manual-add action | **REQUIRED** | **ADMIN_ONLY**, dedicated adjustment must force `manual_flag=1` and preserve reason | **NOT_REQUIRED** |
|
||
|
|
| Duration | **DEVICE_GENERATED** through idempotent ingestion | **REQUIRED** | **NOT_REQUIRED** | **NOT_REQUIRED** |
|
||
|
|
| Portrait | **SYSTEM_GENERATED** | **REQUIRED** | **SYSTEM_GENERATED** refresh/recalculation | **NOT_REQUIRED** |
|
||
|
|
|
||
|
|
## 4. Legacy code versus V2 database compatibility matrix
|
||
|
|
|
||
|
|
| Module | Legacy key/model | V2 key/model | Missing live columns referenced by legacy mapper | Before fix | Target |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| Report | `plan_id + student_id`; allocations embedded | `enrollment_id`; verification event only | `plan_id, student_id, report_status, alloc_dorm_id, alloc_classroom_id, alloc_site_id` | **BROKEN** | Joined read model; no legacy writes |
|
||
|
|
| Checkin | `plan_id + c_id + student_id + lesson times` | `enrollment_id + lesson_id`; sign-in/out and final attendance | `plan_id, c_id, student_id, checkin_time, checkin_status, lesson_start_time, lesson_end_time` | **BROKEN** | Joined read model; no raw CRUD |
|
||
|
|
| Duration | student/plan/course plus `device_type` | enrollment/plan-course/resource plus source event | `student_id, plan_id, c_id, device_type` | **BROKEN** | Joined read model; no manual CRUD |
|
||
|
|
| Portrait | student/plan, average/checkin rate | enrollment, attendance/theory/practical/time | `student_id, plan_id, avg_score, checkin_rate` | **BROKEN** | Joined read model; no manual CRUD |
|
||
|
|
|
||
|
|
## 5. Permission debt
|
||
|
|
|
||
|
|
All four backend controllers use legacy `system:*` permissions while their Vue buttons use a partially different `train:*` family. The formal API contract uses `train:report:*`, `train:attendance:*`, `train:learning:*`, and `train:portrait:*`. This is **LEGACY_PERMISSION_TECH_DEBT** and can block otherwise valid business roles. The reconciliation should update these four read paths only; a global RBAC/menu migration is outside this patch.
|
||
|
|
|
||
|
|
## 6. Architecture decision gate
|
||
|
|
|
||
|
|
`ARCHITECT_DECISION_REQUIRED = NO` for the minimal reconciliation: the frozen contract and live V2 schema agree on identifiers, ownership, read/write semantics, and the removal of legacy fields. This patch does not implement the future transactional report, attendance, learning-ingestion, or portrait-generation workflows; it only makes the retained legacy analytics views safe and truthful.
|
||
|
|
|
||
|
|
## 7. Existing V2 write-action gap
|
||
|
|
|
||
|
|
`EXISTING_V2_WRITE_ACTION_GAP = YES`.
|
||
|
|
|
||
|
|
The frozen contract declares Report confirm, Attendance sign-in, Attendance sign-out, and Attendance adjust actions, but neither the base revision nor this worktree contains their dedicated Controller/Service implementations. This is a pre-existing delivery gap, not a regression caused by converting the legacy analytics generator pages to V2 read models. Implementing these transactional actions remains outside this reconciliation.
|