Student Service
This page covers lumie-backend/modules/student.
Responsibility
The student module owns:
- tenant-scoped student records linked to auth users;
- student lifecycle operations such as activate, deactivate, delete, password reset, and login ID change;
- XLSX/CSV bulk import and CSV export;
- student search and statistics;
- the
StudentRegisteredEventused to trigger cross-module follow-up work.
Source Paths
| Path | Role |
|---|---|
lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.java | Public student HTTP API |
lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/internal/StudentServiceAdapter.java | Internal monolith API implementation |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java | Lifecycle, deletion, bulk import, and event publishing |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentQueryService.java | Query, export, enrollment trend, and dropout summary |
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.java | Apache POI-based XLSX parser and multi-encoding CSV parser |
lumie-backend/modules/student/src/test/java/com/lumie/student/application/service/StudentExcelParserTest.java | Parser coverage for UTF-8, x-windows-949, and CSV edge cases |
lumie-backend/modules/student/src/main/java/com/lumie/student/domain/entity/Student.java | Student aggregate |
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentService.java | Internal lookup and validation contract |
lumie-backend/libs/internal-api/src/main/java/com/lumie/student/api/StudentRegisteredEvent.java | After-commit event contract |
lumie-backend/app/src/main/resources/db/migration/public/V18__rls_baseline.sql | Baseline students table |
lumie-backend/app/src/main/resources/db/migration/public/V91__normalize_student_parent_phone_and_drop_unique_index.sql | Parent-phone blank normalization and guardian-number uniqueness relaxation |
Public Surface
| Surface | Entrypoints |
|---|---|
| Student CRUD | POST /v1/students, GET /v1/students, GET /v1/students/{id}, PATCH /v1/students/{id}, DELETE /v1/students/{id} |
| Lifecycle and credentials | POST /v1/students/{id}/deactivate, POST /v1/students/{id}/reactivate, POST /v1/students/{id}/reset-password, PATCH /v1/students/{id}/login-id |
| Batch lifecycle | POST /v1/students/batch/deactivate, POST /v1/students/batch/reactivate, POST /v1/students/batch/delete |
| Bulk import and export | POST /v1/students/bulk, GET /v1/students/export.csv |
| Statistics | GET /v1/students/statistics/enrollment-trend, GET /v1/students/statistics/dropout-summary |
Student list filters
GET /v1/students accepts the existing isActive, search, pagination, and
sort parameters plus these optional filters:
| Parameter | Meaning |
|---|---|
classId | Returns students with an active enrollment in the class. This takes precedence when combined with hasActiveEnrollment. |
hasActiveEnrollment=true | Returns students with at least one active class enrollment. |
hasActiveEnrollment=false | Returns students with no active class enrollment, which powers the unassigned view. |
The student query service obtains enrollment IDs through the class module's internal API; the student module does not import class domain entities.
Internal Surface
StudentService exports:
- lookup by student ID, phone, or auth user IDs;
- validation of a student inside a tenant;
- user-ID search for keyword-based joins;
- batched lookup helpers for other modules.
The exam module is the most important current consumer because it uses student phone lookups and the StudentRegisteredEvent backfill path.
Aggregate And Data Shape
| Aggregate | Table | Notes |
|---|---|---|
Student | students | Stores user linkage, login ID mirror, contact numbers, school, birth year, memo, and active flag |
Important invariants in Student:
- student and parent phone numbers are normalized to digits on write; blank
parent_phonevalues are stored asNULL; user_login_idmirrors auth-side login identity for efficient lookup;- delete is implemented by deleting the auth user, with database cascade removing the student row.
Runtime Flows
Register-and-backfill flow
What the event publish point looks like
Source anchor:
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentCommandService.java
publishes the event from StudentCommandService.registerStudent:
eventPublisher.publishEvent(
new StudentRegisteredEvent(
saved.getId(),
saved.getPhone(),
tenantSlug,
TenantContextHolder.getRequiredTenantId()
)
);
This event is published after the student row is persisted, and downstream listeners consume it after commit. The event carries both tenantSlug and tenantId so replayed listeners can restore the full RLS tenant context.
Bulk import publishes the same event once for each successfully saved student row.
Key Behaviors
- Student registration and reactivation both perform quota checks through
MetricType.STUDENTS. - Student
phoneremains unique when present and must normalize to an 11-digit mobile number for registration, update, and bulk import.parent_phoneis optional, not unique, and still uses the broader shared phone parser, so one guardian phone number can be attached to multiple student rows. - Single delete requires the student to be inactive, clears active class enrollments through
ClassService.dropActiveEnrollmentsForStudent(...), then deletes the auth user. - Bulk import is a two-pass process:
- pass 1 validates all rows and collects every row error;
- pass 2 creates auth users and student rows only for valid rows.
- The import parser accepts
.xlsxuploads plus.csvuploads decoded in order as UTF-8,x-windows-949, thenEUC-KR.StudentExcelParser.parseCsvRecords(...)keeps trying those charsets until the decoded header matches학생 이름,학생 연락처, rejects malformed quoted CSV fields, caps uploads at 1,000 data rows, and rejects CSV records over 32 columns. - Bulk import rejects text values that could be interpreted as spreadsheet formulas in free-text columns. CSV export also prefixes such stored values with an apostrophe before writing cells.
POST /v1/students/bulkrequiresIdempotency-Key.StudentControllerfingerprints the upload by byte size and SHA-256 content hash before delegating toIdempotencyService.executeOnce(...).- Bulk import requires at least
INSTRUCTORauthority at the controller boundary. - Dropout summary is an approximation based on
updated_atfor inactive rows because the schema has no dedicateddeactivated_atfield.
Representative Contract Example
These representative examples are anchored to StudentExcelParser, StudentExcelParserTest, StudentController, and BulkImportResult.
Template row shape
The first worksheet or CSV file must start with these columns:
학생 이름,학생 연락처,학교명,출생 연도,학부모 연락처,메모
김철수,01012340001,한빛고,2008,01012340002,재원
StudentExcelParserTest confirms that the first data row is reported as rowNumber = 2, because the header occupies row 1. The same row numbering applies to CSV uploads.
StudentExcelParserTest.parsesCsvWhenFilenameHasCsvExtension(...) covers the UTF-8 path, and StudentExcelParserTest.parsesCp949CsvExportedFromKoreanExcel(...) proves that Korean Excel CSV exports encoded as x-windows-949 are accepted by the same parser. StudentExcelParser also includes EUC-KR in the fallback charset list before it rejects the file as an invalid template.
Source anchors:
lumie-backend/modules/student/src/main/java/com/lumie/student/application/service/StudentExcelParser.javalumie-backend/modules/student/src/test/java/com/lumie/student/application/service/StudentExcelParserTest.java
Partial-success bulk import response
POST /v1/students/bulk returns 200 OK even when some rows fail, because the import runs inline and reports row-level errors instead of switching to an async job model.
{
"totalRows": 2,
"successCount": 1,
"failureCount": 1,
"errors": [
{
"rowNumber": 3,
"column": "phone",
"value": "010-12",
"code": "STUDENT_502",
"message": "전화번호 형식이 올바르지 않습니다 (예: 010-1234-5678)"
}
]
}
rowNumber points back to the spreadsheet row label, not the zero-based array index. Once failureCount is non-zero, callers must inspect errors[] instead of treating 200 OK as a full success signal.
Source anchors:
lumie-backend/modules/student/src/main/java/com/lumie/student/adapter/in/web/StudentController.javalumie-backend/modules/student/src/main/java/com/lumie/student/application/dto/response/BulkImportResult.java
Dependencies And Boundaries
| Dependency | Why it exists |
|---|---|
AuthService | Create and delete auth users, reset passwords, and change login IDs |
BillingService | Student quota checks |
ClassService | Drop active enrollments before permanent deletion |
Failure Modes
- Registration and update can fail with
INVALID_PHONE_NUMBERfor a studentphonethat is not 11 digits after normalization or for an invalidparentPhone, and withDUPLICATE_PHONEfor duplicate student phone numbers. - Registration can also fail with
AUTH_OP_FAILEDor quota-related errors before the student row is saved. - Delete fails for active students.
- Bulk import rejects invalid template shape, malformed CSV, over-limit row count, and over-limit CSV column count before row-level insertion begins. For
.csvuploads, that includes files whose bytes do not decode to the expected Korean header under UTF-8,x-windows-949, orEUC-KR. - Bulk import can fail per-row on invalid phone format, duplicate phone, quota exhaustion, unsafe spreadsheet-formula text, auth creation failure, or unexpected insert errors.
- Because auth deletion is the destructive step for permanent delete, a failed auth-side delete leaves the student row in place.
Observability And Quota Behavior
- Lifecycle and batch operations log success counts and failures in
StudentCommandService. - Bulk import returns partial success with row-level errors instead of all-or-nothing rollback.
- The student module is quota-aware, but like the staff module it currently receives a permissive unlimited result from the billing internal adapter.
Verification
./gradlew :modules:student:test
./gradlew :app:test --tests '*Student*'
cd lumie-document/docusaurus && npm run build
Expected success signals:
- Gradle finishes with
BUILD SUCCESSFUL. StudentExcelParserTestandStudentCommandServiceTestpass without failures.- Docusaurus finishes without MDX or broken-link errors for
backend/student-svc.