Mục lục (11 phần)
§1Bản đồ hệ thống
Hệ thống có hai máy. Máy thứ nhất chạy một API duy nhất chứa 5 module, cùng database và kho file. Máy thứ hai chỉ làm một việc: chạy bài làm của sinh viên trong container. Mã không tin cậy không bao giờ chạm tới máy thứ nhất.
POST /internal/submissions/{id}/progress, chỉ nhận từ IP của worker, có khoá dịch vụ); API dùng SignalR đẩy tiếp về trình duyệt.Công nghệ theo tầng
| Tầng | Chọn | Vì sao |
|---|---|---|
| Frontend | React 19 + TypeScript + Vite | Chi tiết ở §8 |
| Backend | .NET 10 LTS, ASP.NET Core Minimal API, EF Core 10 | .NET 8 hết hỗ trợ ngày 10/11/2026, trước buổi bảo vệ; .NET 10 được hỗ trợ tới 11/2028 |
| Database | SQL Server 2022, mỗi module một schema | Đúng đề xuất gốc; schema riêng giữ ranh giới module |
| Kho file | MinIO (tương thích S3) | Worker ở máy khác cần tải file bài nộp qua URL ký sẵn, không dùng ổ đĩa chung |
| Hàng đợi | Hangfire + storage SQL Server | Có sẵn retry, dashboard, tự trả job khi worker chết; không cần thêm Redis |
| Thời gian thực | SignalR | Đẩy trạng thái chấm, dự phòng bằng polling |
| Sandbox | Docker Engine + Docker.DotNet | Điều khiển container từ C# |
| AI | Microsoft.Extensions.AI (IChatClient) | Một interface cho OpenAI, Gemini và bản giả lập khi test |
| Proxy | Caddy | Tự cấp chứng chỉ HTTPS, cấu hình vài dòng |
| Giám sát | Serilog + Seq, health checks | Tra log theo submissionId |
(1) Nâng từ .NET 8 lên .NET 10 vì lý do vòng đời hỗ trợ ở trên. (2) Hangfire dùng SQL Server làm storage thay cho Redis: bớt một thành phần phải vận hành. Redis chỉ thêm khi cần chạy nhiều API instance (SignalR backplane).
§2Quy tắc giữa các module
Modular monolith chỉ có giá trị khi ranh giới được giữ. Ba quy tắc dưới đây được kiểm tra tự động trong CI, không dựa vào lời hứa.
- Mỗi module sở hữu dữ liệu của mình. Một
DbContextvà một schema riêng (identity,assess,submit,ai,scoring). Không join chéo schema. - Gọi đồng bộ chỉ qua project
Contracts. Module A chỉ được tham chiếuAsap.Modules.B.Contracts(interface + DTO), không bao giờ tham chiếu phần bên trong của B. - Phản ứng với việc xảy ra ở module khác qua sự kiện. Sự kiện được ghi vào bảng outbox trong cùng transaction, rồi một background service phát đi mỗi 2 giây. Không mất sự kiện khi tiến trình chết giữa chừng.
// Cấu trúc bên trong mỗi module Asap.Modules.Assessment/ Api/ # endpoint Minimal API, map DTO Application/ # use case, validator (FluentValidation), mapper (Mapperly) Domain/ # entity, quy tắc nghiệp vụ, sự kiện — không phụ thuộc EF Infrastructure/ # AssessmentDbContext, cấu hình EF, outbox Asap.Modules.Assessment.Contracts/ # IAssessmentReader, DTO, sự kiện công khai
Ai cung cấp gì, ai nghe gì
| Module | Cung cấp (Contracts) | Dùng của module khác | Phát sự kiện | Nghe sự kiện |
|---|---|---|---|---|
| M1 Identity | ICurrentUser, IUserDirectory | — | UserCreated | — |
| M2 Assessment | IAssessmentReader (bản đã publish, test case, rubric, ItemSkill) | M1 | AssessmentPublished, EvaluationRecorded (quiz) | AttemptStarted |
| M3 Submission | IAttemptReader | M1, M2 | AttemptStarted, AttemptFinished, EvaluationRecorded (test, rule) | — |
| M4 AI | IAiEvaluator (worker gọi trực tiếp) | M1, M2 | EvaluationRecorded (AI), ReviewRequested | AttemptFinished |
| M5 Scoring | IResultReader, ISkillProfileReader | M1, M2 | ResultFinalized, SkillScoreChanged | EvaluationRecorded, ScoreOverridden, AttemptFinished |
Hợp đồng quan trọng nhất của cả hệ thống là sự kiện EvaluationRecorded. Bốn nguồn chấm (quiz, test, rule, AI, và cả giảng viên khi ghi đè) đều phát cùng một dạng này, nhờ đó Score Engine không cần biết loại bài:
public sealed record EvaluationRecorded( Guid AttemptId, Guid ItemId, Guid CriterionId, EvaluationSource Source, // Test | Rule | Ai | Human decimal Score, // 0–100, đã quy về thang chung string Evidence, // "17/20 test đạt", trích dẫn từ bài làm… DateTimeOffset At);
Thư viện dùng chung cho mọi module backend
| Thư viện | Dùng để | Ghi chú |
|---|---|---|
| FluentValidation | Kiểm tra dữ liệu đầu vào | Trả lỗi dạng ProblemDetails (RFC 9457) |
| Mapperly | Map entity ↔ DTO | Sinh code lúc build, giấy phép MIT |
| Dispatcher tự viết | Phát sự kiện nội bộ + outbox | Khoảng 100 dòng code; MediatR và AutoMapper đã chuyển sang giấy phép thương mại từ 2025 |
| Microsoft.AspNetCore.OpenApi + Scalar | Sinh tài liệu OpenAPI và trang thử API | Frontend sinh client TypeScript từ file này |
| NetArchTest | Test kiến trúc | Fail CI nếu module tham chiếu phần trong của module khác |
§3M1 · Identity & Platform SV1
Trả lời hai câu hỏi cho mọi request: ai đang gọi và họ có được làm việc này không. Đồng thời ghi lại mọi thao tác nhạy cảm.
Luồng 1 — Đăng nhập và làm mới token
Access token không nằm trong localStorage nên script lạ không đọc được. Refresh token xoay vòng mỗi lần dùng; nếu một token đã thu hồi xuất hiện lại, đó là dấu hiệu bị đánh cắp, hệ thống thu hồi toàn bộ chuỗi và buộc đăng nhập lại.
Luồng 2 — Một request đi qua những lớp nào
Phân quyền: vai trò → quyền → policy
Không kiểm tra theo tên vai trò rải rác trong code. Mỗi hành động là một quyền; vai trò chỉ là tập quyền; endpoint yêu cầu quyền. Quyền sở hữu (sinh viên chỉ xem kết quả của mình) kiểm tra bằng IAuthorizationHandler theo tài nguyên.
| Quyền | Admin | Instructor | Student |
|---|---|---|---|
| users.manage | ✓ | ||
| ai.configure | ✓ | ||
| assessment.author · assessment.publish | ✓ | ||
| result.review · result.override | ✓ | ||
| attempt.take | ✓ | ||
| result.read.own · skill.read.own | ✓ | ||
| audit.read | ✓ |
Thành phần và công nghệ
| Thành phần | Công nghệ | Việc |
|---|---|---|
| UserStore, PasswordHasher | ASP.NET Core Identity (AddIdentityCore + EF stores) | Lưu người dùng, băm PBKDF2, khoá tài khoản khi sai nhiều lần |
| TokenService | Microsoft.IdentityModel.JsonWebTokens | Ký JWT (HMAC-SHA256 hoặc RSA), claim sub, role, perm |
| RefreshTokenService | EF Core | Lưu hash của token, xoay vòng, phát hiện dùng lại |
| PermissionPolicyProvider | ASP.NET Core Authorization | Tự tạo policy từ tên quyền: RequirePermission("result.override") |
| Rate limiting | Microsoft.AspNetCore.RateLimiting (có sẵn) | Login 5 lần/phút/IP; API chung 100 lần/phút/người |
| AuditInterceptor | EF Core SaveChangesInterceptor | Ghi ai, lúc nào, IP, entity, giá trị trước/sau cho các entity đánh dấu [Audited] |
| CurrentUser | Contracts: ICurrentUser | Module khác lấy UserId, quyền mà không đọc token trực tiếp |
API: POST /auth/login · /auth/refresh · /auth/logout · /api/users · /api/roles · GET /api/admin/audit-logs
Test bắt buộc: sai mật khẩu lần thứ 6 trong 1 phút → 429 · JWT hết hạn hoặc sai chữ ký → 401 · sinh viên gọi API giảng viên → 403 · sinh viên đọc kết quả người khác → 404 · refresh token dùng lại → cả chuỗi bị thu hồi.
§4M2 · Assessment & Quiz SV2
Nơi giảng viên cấu hình mọi thứ sẽ được chấm, và nơi chạy toàn bộ phần trắc nghiệm. Học theo mô hình ngân hàng câu hỏi của Moodle.
Luồng 1 — Vòng đời một bài đánh giá
Luồng 2 — Một lượt làm trắc nghiệm
EvaluationRecorded như mọi loại bài khác, nên M5 xử lý giống nhau. Hết giờ mà sinh viên chưa nộp: một job Hangfire chạy mỗi phút tự nộp các lượt quá hạn.Thành phần và công nghệ
| Thành phần | Công nghệ | Việc |
|---|---|---|
| QuestionBankService | EF Core, bảng QuestionVersion chỉ INSERT | Tạo, sửa (sinh phiên bản mới), tìm theo kỹ năng và độ khó |
| QuestionImporter | ClosedXML (Excel), parser GIFT tự viết | Nhập hàng loạt; báo lỗi theo từng dòng |
| BlueprintValidator | C# thuần | Kiểm tra ngân hàng đủ câu cho từng ô của ma trận đề |
| PaperGenerator | C# thuần, Random có seed | Rút câu theo ma trận, trộn thứ tự, không trùng câu |
| QuizScorer + IScoringStrategy | Strategy pattern | AllOrNothing, Partial, PartialWithPenalty |
| TestCaseParser | YamlDotNet + JsonSchema.Net | Đọc YAML test case REST, kiểm tra đúng schema trước khi lưu |
| RubricValidator | FluentValidation | Tổng trọng số = 100%, mỗi tiêu chí có nguồn chấm và mô tả mức |
| DryRunService | Gọi M3 qua Contracts | Nộp lời giải mẫu như một bài nộp thật; phải đạt 100% mới cho publish |
| AutoSubmitJob · ItemAnalysisJob | Hangfire recurring | Tự nộp lượt quá hạn (mỗi phút); tính thống kê câu hỏi sau hạn chung |
| Cột JSON | EF Core ToJson() | Lưu OptionOrder, RulesJson, mô tả mức rubric mà không cần bảng phụ |
// Chấm câu nhiều đáp án — chính sách mặc định điểm = trọng số × max(0, số_đúng_đã_chọn / k − số_sai_đã_chọn / (n − k)) // k: số đáp án đúng · n: tổng số lựa chọn · câu một đáp án luôn là đúng hết / 0
API: /api/skills · /api/assessments (+ /publish, /dry-run, /assignments) · /api/questions (+ /import) · /api/items/{id}/quiz-config | testcases | rubric · /api/attempts/{id}/quiz/start | answers/{q} | submit | result · /api/items/{id}/item-analysis
Test bắt buộc: 7 tình huống chấm của QuizScorer · ngân hàng thiếu câu cho ma trận → không cho publish · sửa câu hỏi sau publish không đổi điểm bài cũ · DTO gửi sinh viên không bao giờ có IsCorrect · câu trả lời gửi sau hạn 30 giây bị từ chối.
§5M3 · Submission & chấm tự động SV3
Nhận bài, xếp hàng, rồi build và chạy bài trong container trên máy worker. Đây là module khó nhất, học theo cách Gradescope chấm trong Docker.
--network none → OutputComparator so output → ghi kết quả. Lỗi của sinh viên (không build được, quá giờ) là kết quả chấm hợp lệ; chỉ lỗi hệ thống mới retry.Thành phần và công nghệ
| Thành phần | Công nghệ | Việc |
|---|---|---|
| SubmissionService | API · EF Core | Kiểm tra chủ lượt làm, hạn nộp, dung lượng; lưu file; đặt Queued; ghi outbox |
| ArtifactStore | Minio .NET SDK | Lưu file theo submissionId, tạo URL ký sẵn sống 10 phút cho worker |
| GradingJob | Hangfire · [Queue("grading")] · [AutomaticRetry(Attempts = 3)] | Điểm vào của worker; bỏ qua nếu submission đã chấm (chạy lại không sao) |
| DockerRunner | Docker.DotNet | Tạo mạng Internal = true, build từ tar, tạo container với Memory, NanoCPUs, PidsLimit, ReadonlyRootfs, CapDrop = ALL, kill khi quá giờ |
| HealthProbe | Microsoft.Extensions.Http.Resilience | Gọi /health mỗi 2 giây, tối đa 60 giây |
| HttpTestRunner | HttpClient + JsonPath.Net + JsonSchema.Net | Chạy test tuần tự, thay biến đã capture, ghi lại toàn bộ request/response |
| OutputComparator | C# thuần | So output bài stdin/stdout: bỏ khoảng trắng cuối, sai số số thực |
| RuleEngine | Mỗi luật một class IApiRule | Chấm trên lưu lượng đã ghi: tạo mới trả 201 + Location; lỗi trả ProblemDetails; lỗi 500 không lộ stack trace; thiếu token trả 401; response không chứa trường password |
| ProgressReporter | HttpClient → endpoint nội bộ của API | Báo từng bước (build, health, test 12/20…) để SignalR đẩy lên giao diện |
Runner image dựng sẵn: asap-runner-python:3.12 · asap-runner-dotnet:10 · base image cho starter template REST (ASP.NET Core, Node/Express) đã có sẵn thư viện phổ biến để build nhanh.
API: POST /api/attempts · POST /api/attempts/{id}/finish · POST /api/submissions → 202 · GET /api/submissions/{id} · POST /api/submissions/precheck (nút "Kiểm tra build", không tính điểm) · POST /internal/submissions/{id}/progress
Test bắt buộc: bộ bài nộp độc hại (vòng lặp vô hạn, fork bomb, cấp phát 10 GB, đọc /etc/passwd, gọi ra Internet, in 1 GB output) phải bị chặn và vẫn nhận điểm 0 hợp lệ · kill worker giữa chừng → job được chạy lại · lời giải mẫu đạt 20/20. GitHub Actions có sẵn Docker nên chạy được các test này trên CI.
Xác nhận worker gọi được vào container nằm trên mạng Docker --internal (container không ra Internet nhưng máy chủ vẫn vào được). Nếu không, chạy HTTP test runner như một container phụ cùng mạng với bài làm.
§6M4 · AI Evaluation & Interview SV4
Hai việc: chạy phiên phỏng vấn nhiều lượt, và chấm những tiêu chí không kiểm bằng test được. Học cách hỏi của CodeSignal AI Interviewer và cách chấm theo rubric của Aced; khác biệt là mọi điểm AI đều đi qua bộ kiểm soát và có giảng viên duyệt.
Luồng 1 — Chấm một bài bằng AI có kiểm soát
EvaluationRecorded mới có nguồn Human.Luồng 2 — Một phiên phỏng vấn
Mỗi lượt hỏi–đáp gọi AI đồng bộ trong request (timeout 30 giây, giao diện hiện "AI đang soạn câu hỏi…"). Việc chấm cả phiên chạy nền bằng Hangfire trên queue ai ở máy API; máy worker chỉ dành cho queue grading.
Thành phần và công nghệ
| Thành phần | Công nghệ | Việc |
|---|---|---|
| Chat client | Microsoft.Extensions.AI · IChatClient + Microsoft.Extensions.AI.OpenAI | Một interface cho OpenAI; Gemini qua endpoint tương thích OpenAI; FakeChatClient cho test và CI |
| Structured output | GetResponseAsync<T>() + JsonSchema.Net | Buộc AI trả đúng kiểu C#; kiểm tra lại bằng schema trước khi tin |
| PromptTemplateStore | EF Core, có phiên bản | Mỗi lần chấm lưu lại dùng template phiên bản nào |
| EvaluationGuard | C# thuần | Schema, độ lệch hai lần, bằng chứng có thật trong bài, cờ nghi prompt injection |
| InterviewEngine | Máy trạng thái | Asking → Awaiting → FollowUp(n) → Next → Completed |
| Resilience | Microsoft.Extensions.Http.Resilience | Retry khi 429/5xx có giãn cách, timeout 60 giây, ngắt mạch khi provider lỗi liên tục |
| TokenBudget · AiCallLog | EF Core | Giới hạn token mỗi bài; lưu model, token, thời gian, response gốc |
| GoldSetRunner | Test xUnit gắn nhãn [Trait("ai","gold")] | Chạy lại AI trên bộ bài GV đã chấm, tính MAE; chạy tay khi đổi prompt hoặc model |
// Kết quả AI phải trả cho mỗi tiêu chí { "criterionId": "accuracy", "score": 7.5, // 0–10, bước 0.5 "evidence": ["PUT thay thế toàn bộ tài nguyên…"], // phải tìm thấy trong bài làm "feedback": "Đúng về PUT, chưa nêu tính idempotent." }
API: POST /api/interviews · POST /api/interviews/{id}/answers · GET /api/reviews?status=NeedsReview · POST /api/reviews/{id}/approve | override · /api/admin/ai-providers · /api/admin/prompt-templates
Test bắt buộc: FakeChatClient trả JSON sai → gọi lại 1 lần rồi NeedsReview · hai lần lệch 2 điểm → NeedsReview · trích dẫn không có trong bài → bị loại · bài làm chứa "ignore previous instructions" → gắn cờ · provider trả 429 → retry có giãn cách.
§7M5 · Scoring & Skill Profile SV5
Nơi mọi nguồn chấm hội tụ thành một con số giải thích được. Module này không gọi ai; nó chỉ nghe sự kiện và tính lại.
Thành phần và công nghệ
| Thành phần | Công nghệ | Việc |
|---|---|---|
| EvaluationProjection | Handler sự kiện · EF Core | Ghi Evaluation, khoá duy nhất theo (lượt làm, tiêu chí, nguồn) nên nhận trùng sự kiện không sao |
| ItemResult / AssessmentResult Calculator | C# thuần | Công thức tầng 2 và 3; trả kèm cây truy vết |
| FinalizationPolicy | C# thuần | Chỉ chốt khi mọi item có điểm và không còn tiêu chí NeedsReview |
| SkillScoreCalculator | C# thuần | Tầng 4: mỗi item lấy lượt gần nhất; cần ít nhất 2 item mới hiển thị |
| SnapshotWriter | EF Core | Chỉ ghi snapshot khi điểm thay đổi |
| RecommendationRules | Bảng luật (điều kiện JSON → lộ trình học) | Ví dụ: Security < 60 → lộ trình bảo mật API; kèm lý do "thấp nhất, tăng chậm nhất" |
| OverrideService | M1 audit | Bắt buộc lý do; phát ScoreOverridden và một Evaluation nguồn Human |
| ReportExporter | CsvHelper | Xuất điểm lớp ra CSV; PDF là mục mở rộng |
API: GET /api/results/{id} · GET /api/results/{id}/trace (cây kỹ năng → item → tiêu chí → bằng chứng, phục vụ yêu cầu "truy vết trong 2 lần bấm") · PUT /api/results/{id}/criteria/{cid}/override · GET /api/skill-profiles/me · GET /api/skill-profiles/{studentId}/progress · GET /api/recommendations/me · GET /api/classes/{id}/skill-stats
Test bắt buộc: phiếu tính mẫu phải ra đúng 78.50 · điểm luôn trong 0–100 · tổng trọng số khác 100% bị từ chối · ghi đè xong tính lại đúng · nhận cùng một sự kiện hai lần không đổi kết quả · chỉ có 1 item thì hiện "chưa đủ dữ liệu".
§8Frontend
Một ứng dụng React chia thư mục theo đúng 5 module của backend. Người làm module nào sở hữu cả màn hình của module đó.
| Việc | Thư viện | Ghi chú |
|---|---|---|
| Nền tảng | React 19 + TypeScript + Vite | |
| Điều hướng | React Router | Nhánh route theo vai trò, tải lười từng module |
| Gọi API, cache | TanStack Query + openapi-typescript + openapi-fetch | Kiểu dữ liệu sinh từ OpenAPI của backend: đổi API mà quên sửa frontend thì build lỗi ngay |
| Giao diện | Ant Design | Bảng, form, layout quản trị có sẵn |
| Form | react-hook-form + zod | Form soạn đề, rubric nhiều trường |
| Soạn code, YAML | @monaco-editor/react | Dùng cho cả ô viết code của SV và ô soạn test case YAML của GV |
| Biểu đồ | Recharts | Radar kỹ năng, đường tiến bộ |
| Thời gian thực | @microsoft/signalr | Trạng thái chấm |
| Nội dung đề | react-markdown + rehype-highlight | Đề bài có đoạn code |
| Kiểm thử | Vitest + Testing Library + MSW · Playwright | MSW giả API để làm giao diện trước khi backend xong |
web/src/ app/ # router, provider, layout theo vai trò, trang lỗi modules/ identity/ # đăng nhập, người dùng, audit — SV1 assessment/ # kỹ năng, soạn đề, test case, rubric — SV2 quiz/ # làm trắc nghiệm, tự lưu — SV2 submission/ # nộp code, trạng thái chấm — SV3 interview/ # chat phỏng vấn, màn GV duyệt — SV4 results/ # kết quả, truy vết, hồ sơ kỹ năng — SV5 shared/ api/ # client sinh từ OpenAPI (không sửa tay) ui/ # component dùng chung: ScoreChip, SourceChip, ExamLayout hooks/ # useAuth, useSubmissionStatus, useServerDeadline
Bốn luồng phía trình duyệt cần làm cẩn thận
| Luồng | Cách làm |
|---|---|
| Hết hạn token | Interceptor bắt 401, gọi /auth/refresh một lần cho mọi request đang chờ, rồi gửi lại. Refresh thất bại → về trang đăng nhập |
| Tự lưu trắc nghiệm | Mỗi lần chọn gửi một mutation; đồng thời lưu bản sao vào localStorage; mạng rớt thì hiện "Đang lưu lại…" và gửi lại khi có mạng |
| Trạng thái chấm | useSubmissionStatus tham gia nhóm SignalR của bài nộp; mất kết nối thì chuyển sang hỏi lại mỗi 3 giây |
| Chế độ làm bài | ExamLayout ẩn menu, cảnh báo khi đóng tab; đồng hồ tính từ deadline server trả về, đồng bộ lại mỗi 60 giây |
| Vai trò | Route chính |
|---|---|
| Student | / · /assignments · /attempts/:id (chế độ làm bài) · /results/:id · /skills |
| Instructor | /teach/skills · /teach/assessments/:id/edit · /teach/questions · /teach/reviews · /teach/classes/:id/stats |
| Admin | /admin/users · /admin/ai · /admin/audit · /admin/jobs (Hangfire dashboard) |
§9Hạ tầng, CI/CD, giám sát SV1
Triển khai trên hai máy
| Máy | Dịch vụ (Docker Compose) | Cấu hình đề xuất |
|---|---|---|
| Máy chính | caddy (HTTPS + phục vụ file tĩnh của web) · api (kèm Hangfire server cho queue ai, maintenance) · sqlserver · minio · seq | 4 vCPU · 8 GB RAM |
| Máy worker | worker (Hangfire server cho queue grading, 4 job song song) · Docker Engine · runner image dựng sẵn | 4 vCPU · 8 GB RAM; chỉ mở kết nối ra SQL Server, MinIO, API nội bộ, AI provider |
Pipeline
Giám sát và cấu hình
- Log: Serilog ghi vào Seq; mọi log của một lần chấm mang
submissionIdđể tra một phát ra hết. - Health check:
/health/livevà/health/ready(SQL Server, MinIO, Hangfire, Docker ở worker). - Job: Hangfire dashboard chỉ Admin mở được; thấy job lỗi, job đang retry, DeadLetter.
- Bí mật: chuỗi kết nối qua biến môi trường và GitHub Actions secrets; khoá AI do Admin nhập, lưu mã hoá bằng ASP.NET Core Data Protection.
§10Một lượt làm bài đi qua các module
Đọc bảng này để thấy các module nối với nhau thế nào trong đúng một kịch bản demo: bạn An làm "Backend Developer – Level 1".
| Bước | Module | Việc xảy ra | Công nghệ chính |
|---|---|---|---|
| 1 | M2 | GV kiểm tra cấu hình, chạy lời giải mẫu, publish v1 | FluentValidation · dry-run qua M3 |
| 2 | M3 | An bắt đầu; tạo Attempt với deadline | Outbox → AttemptStarted |
| 3 | M2 | Sinh đề, tự lưu, nộp, chấm 8/10 | PaperGenerator · QuizScorer → EvaluationRecorded |
| 4 | M3 | Nộp file zip dự án REST; trả 202 | MinIO · Hangfire queue grading |
| 5 | M3 · Worker | Build, chạy, health check, 20 test HTTP, luật thiết kế | Docker.DotNet · JsonPath.Net |
| 6 | M4 · từ Worker | AI nhận xét chất lượng mã, qua bộ kiểm soát | Microsoft.Extensions.AI · EvaluationGuard |
| 7 | M3 → Web | Giao diện thấy "Chạy test HTTP 12/20" | Endpoint nội bộ → SignalR |
| 8 | M4 | Phỏng vấn 5 câu, AI hỏi nối tiếp; chấm nền | InterviewEngine · Hangfire queue ai |
| 9 | M4 · M5 | GV duyệt điểm phỏng vấn | ScoreOverridden · AuditLog |
| 10 | M5 | Tính 80 / 79.25 / 76.25 → 78.5; chốt kết quả | Calculator thuần → ResultFinalized |
| 11 | M5 | Cập nhật REST API 78, Security 55; lưu snapshot; gợi ý học Security | SkillScoreChanged · RecommendationRules |
| 12 | Web | An xem kết quả, bấm vào từng tiêu chí, xem hồ sơ kỹ năng | TanStack Query · Recharts |
§11Thứ tự xây & mốc tích hợp
Tuần 1–2: chốt hợp đồng và thử bốn điểm rủi ro
Trước khi ai viết tính năng, cả nhóm chốt các project Contracts, sự kiện EvaluationRecorded, enum trạng thái bài nộp và file OpenAPI đầu tiên. Song song, mỗi rủi ro kỹ thuật được thử bằng một đoạn code nhỏ vứt đi được:
| Spike | Người | Câu hỏi cần trả lời |
|---|---|---|
| Docker từ C# | SV3 | Build và chạy container có giới hạn tài nguyên bằng Docker.DotNet; worker gọi được vào container trên mạng --internal không? |
| AI trả JSON | SV4 | Structured output qua Microsoft.Extensions.AI với OpenAI và Gemini; hai lần chấm cùng một bài lệch bao nhiêu? |
| Token và cookie | SV1 | Refresh token trong cookie httpOnly có chạy được giữa Vite dev server và API (CORS, SameSite) không? |
| Outbox và cột JSON | SV2 · SV5 | Dispatcher outbox tự viết và EF Core ToJson() trên SQL Server có đủ dùng không? |
Mỗi sprint, mỗi module giao gì
| Sprint | M1 · SV1 | M2 · SV2 | M3 · SV3 | M4 · SV4 | M5 · SV5 |
|---|---|---|---|---|---|
| S1 · T3–4 | Đăng nhập, RBAC | CRUD kỹ năng, bài đánh giá | Attempt; worker nhận job thử | IChatClient + bản giả lập | Nhận EvaluationRecorded |
| S2 · T5–6 | Audit, rate limit | Trắc nghiệm end-to-end | Upload, MinIO, trạng thái | Kho prompt có phiên bản | ItemResult, AssessmentResult, trang kết quả v1 |
| S3 · T7–8 | CI/CD, bản demo online | Ngân hàng câu hỏi, nhập Excel | Sandbox, runner stdin/stdout | AI nhận xét mã + bộ kiểm soát | API truy vết |
| S4 · T9–10 | Gia cố bảo mật | Soạn test case YAML, dry-run | Runner REST, rule engine | Interview engine | SkillScore |
| S5 · T11–12 | Màn quản trị người dùng | Rút đề theo ma trận | Retry, DeadLetter, tiến độ SignalR | Màn GV duyệt, gold set | Snapshot, radar, biểu đồ tiến bộ |
| S6 · T13–14 | Màn audit log | Thống kê câu hỏi | Bộ bài độc hại hoàn chỉnh | Chống prompt injection, giới hạn token | Gợi ý học, dashboard lớp |
| S7 · T15–16 | Cả nhóm: kiểm thử tải k6, kiểm thử bảo mật, sửa lỗi, kịch bản demo §10, báo cáo | ||||
Hết tuần 8 mà runner stdin/stdout chưa chạy end-to-end: chuyển phần stdin/stdout sang Judge0 tự cài, SV3 dồn sức cho runner REST. Ngân hàng câu hỏi rút theo ma trận và thống kê câu hỏi là hai mục cắt đầu tiên nếu SV2 phải hỗ trợ người khác.