9.7 KiB
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.