Files
gongxue-base/docs/superpowers/plans/2026-07-06-student-archive.md

8.2 KiB

学生档案子系统 Implementation Plan

For agentic workers: Use subagent-driven-development. Steps use checkbox syntax.

Goal: Rebuild the student profile/report subsystem: 6 new entity tables, CRUD APIs, aggregate query, PDF report generation (pdfkit), frontend profile page, multi-enrollment comparison.

Architecture: New ArchiveModule aggregates 6 sub-entities under one API surface. GET /archive/:studentId returns the full profile. GET /archive/:studentId/report generates PDF. Use pdfkit (already in deps) for PDF; ECharts server-side SVG for charts.

Tech Stack: NestJS 11 + TypeORM + pdfkit + ECharts (SSR via echarts npm) + React 19 + Ant Design 6

Global Constraints

+- All tables have department_id for campus scope isolation +- All write operations log via OperationLogsService +- PDF uses pdfkit (NOT puppeteer) — already in bill export +- Charts in PDF rendered as static SVG via echarts SSR +- Sensitive fields (phone/idNumber) masked in API responses, unmasked in PDF +- Follow existing NestJS module structure: archive/ with entity/dto/service/controller +- Frontend follows existing page patterns (Students page as reference)


Entities Design

student_profiles — 扩展档案

id (PK), student_id FK UNIQUE, target_college, target_major, subject_direction,
grade, campus_location, profile_date (建档日期), notes,
department_id, created_at, updated_at

student_enrollments — 报读记录

id (PK), student_id FK, course_category (课程类别), class_type (班型: culture/professional/bootcamp),
class_name, head_teacher, subject_teacher, start_date, end_date, status,
department_id, created_at, updated_at

exam_scores — 考试成绩

id (PK), student_id FK, enrollment_id FK (nullable, links to enrollment),
exam_type (周测/月测/模考/入学测), exam_name, subject, score (decimal),
class_avg (decimal), rank, exam_date,
department_id, created_at

learning_records — 学情记录

id (PK), student_id FK, record_date, record_type (课堂表现/作业/沟通/其他),
content (text), follow_up_method, next_step,
department_id, created_at

result_archives — 录取归档

id (PK), student_id FK, culture_final_score (decimal), professional_final_score (decimal),
admission_status (已录取/未录取/待定), admitted_college, admitted_major,
department_id, created_at, updated_at

archive_attachments — 附件

id (PK), student_id FK, category (成绩截图/录取截图/协议/其他),
file_name, file_path, file_size, mime_type,
department_id, created_at

student_reports — 报告版本

id (PK), student_id FK, snapshot_data (JSON  frozen copy of all profile data at generation time),
html_content (text  rendered HTML), pdf_path, generated_at,
department_id

Phase 1: Entities + Module Skeleton

Files: +- Create: apps/server/src/entities/student-profile.entity.ts +- Create: apps/server/src/entities/student-enrollment.entity.ts +- Create: apps/server/src/entities/exam-score.entity.ts +- Create: apps/server/src/entities/learning-record.entity.ts +- Create: apps/server/src/entities/result-archive.entity.ts +- Create: apps/server/src/entities/archive-attachment.entity.ts +- Create: apps/server/src/entities/student-report.entity.ts +- Modify: apps/server/src/entities/index.ts +- Create: apps/server/src/archive/archive.module.ts +- Create: apps/server/src/archive/archive.service.ts +- Create: apps/server/src/archive/archive.controller.ts +- Create: apps/server/src/archive/dto/archive.dto.ts +- Modify: apps/server/src/app.module.ts

All entities follow existing TypeORM patterns with @Entity, @PrimaryGeneratedColumn, @Column, @ManyToOne(Student), @CreateDateColumn.

ArchiveModule imports TypeOrmModule.forFeature([all 6 entities]), is registered in AppModule.

ArchiveService provides:

  • getProfile(studentId) — joins all 6 tables, returns aggregate
  • saveProfile(studentId, dto) — upsert student_profiles
  • CRUD for each sub-entity (enrollments, scores, records, results, attachments)
  • generateReport(studentId) — produces PDF

Phase 2: CRUD APIs

ArchiveController endpoints:

Method Path Description
GET /archive/:studentId Full profile aggregate
PUT /archive/:studentId/profile Upsert student_profiles
POST /archive/:studentId/enrollments Add enrollment
PUT /archive/enrollments/:id Edit enrollment
DELETE /archive/enrollments/:id Delete enrollment
POST /archive/:studentId/exam-scores Add exam score
PUT /archive/exam-scores/:id Edit exam score
DELETE /archive/exam-scores/:id Delete exam score
POST /archive/:studentId/learning-records Add learning record
PUT /archive/learning-records/:id Edit learning record
DELETE /archive/learning-records/:id Delete learning record
PUT /archive/:studentId/result Upsert result archive
POST /archive/:studentId/attachments Upload attachment (multipart)
DELETE /archive/attachments/:id Delete attachment
GET /archive/:studentId/report Generate & download PDF

All write endpoints log via OperationLogsService (module: '学生档案').


Phase 3: PDF Report Generation

Use pdfkit. One file: apps/server/src/archive/archive-report.service.ts.

Report structure (multi-page A4):

  1. 封面 — name, studentNo, subjectDirection, targetCollege/targetMajor, headTeacher, profileDate
  2. 基础信息 — personal info table + enrollment comparison (culture vs professional side-by-side)
  3. 入学测评与阶段概览 — first exam scores, highest scores, improvement (bar chart via echarts SVG)
  4. 出勤记录 — attendance summary (pie: present/absent/late/leave), daily matrix
  5. 文化课测评 — all culture exam scores table, subject breakdown bar
  6. 专业课测评 — all professional exam scores table, learning records list

The multi-enrollment comparison (PRD 2.2): when student has 2+ enrollments (e.g., culture + professional), each gets its own column in the tables and its own chart section.

Key implementation:

  • generateReport(studentId) — orchestrates data gathering, builds PDF sections
  • Helper: renderAttendancePie(records) → SVG buffer → embedded in PDF
  • Helper: renderScoreBar(scores) → SVG buffer → embedded in PDF
  • Charts: use echarts npm package, render to SVG string, convert to buffer, embed via doc.image()

Phase 4: Frontend Student Profile Page

Files: +- Create: apps/admin/src/pages/StudentProfile/index.tsx +- Modify: apps/admin/src/App.tsx (add route /students/:id/profile)

Page layout:

  • 顶部 — Student info card (name, phone masked, idNumber masked, status, tenant — reusing Students page data)
  • Tabs: 基础档案 / 报读记录 / 考试成绩 / 学情记录 / 录取结果 / 附件
  • 基础档案 Tab — Form: 目标院校、目标专业、科类方向、年级、校区、建档日期
  • 报读记录 Tab — Table + Add modal: 课程类别、班型、班级名、班主任、任课老师、开/结课日期
  • 考试成绩 Tab — Table + Add/Edit modal: 类型、名称、科目、分数、班级平均、排名、日期
  • 学情记录 Tab — Table + Add modal: 日期、类型、内容、跟进方式、下一步
  • 录取结果 Tab — Form: 文化课最终成绩、专业课最终成绩、录取状态、录取院校、录取专业
  • 附件 Tab — Upload list: 分类、文件名、大小、删除
  • 操作栏 — "生成档案报表" button → downloads PDF

Phase 5: Multi-Enrollment PDF Comparison

PRD 2.2 specific: when 2+ enrollments exist, the PDF report must show them side-by-side:

  • Cover page: list all class_types
  • Score tables: columns per enrollment
  • Separate chart sections for culture vs professional

This is handled in the PDF generation logic — the archive-report.service.ts builds sections dynamically based on enrollment count.


Execution Order

Phase 1→2→4→3→5 (entities→CRUD→frontend→PDF→comparison). Phases 3+5 are combined in the report service.

Total: 5 phases, ~12 files created, ~3 files modified.