chore: commit remaining integration files, PM2 single instance, PRD doc
This commit is contained in:
188
docs/superpowers/plans/2026-07-06-student-archive.md
Normal file
188
docs/superpowers/plans/2026-07-06-student-archive.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 学生档案子系统 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 — 扩展档案
|
||||
```sql
|
||||
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 — 报读记录
|
||||
```sql
|
||||
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 — 考试成绩
|
||||
```sql
|
||||
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 — 学情记录
|
||||
```sql
|
||||
id (PK), student_id FK, record_date, record_type (课堂表现/作业/沟通/其他),
|
||||
content (text), follow_up_method, next_step,
|
||||
department_id, created_at
|
||||
```
|
||||
|
||||
### result_archives — 录取归档
|
||||
```sql
|
||||
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 — 附件
|
||||
```sql
|
||||
id (PK), student_id FK, category (成绩截图/录取截图/协议/其他),
|
||||
file_name, file_path, file_size, mime_type,
|
||||
department_id, created_at
|
||||
```
|
||||
|
||||
### student_reports — 报告版本
|
||||
```sql
|
||||
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.**
|
||||
Reference in New Issue
Block a user