Tài liệu thiết kế kỹ thuật · v1 · 13/09/2026 · đi kèm bản phân tích đề tài

Thiết kế luồng và công nghệ cho từng module

ASAP · Automated Skill Assessment Platform

Mỗi module có sơ đồ luồng chính, thành phần bên trong, thư viện cụ thể, API, sự kiện trao đổi với module khác và cách kiểm thử. Thành viên nào phụ trách module nào có thể đọc đúng phần của mình rồi bắt tay vào code.

Kiến trúc
Modular monolith + 1 worker chấm bài
Nền tảng
.NET 10 LTS · React 19 · SQL Server · Docker
Phạm vi
5 module nghiệp vụ · frontend · hạ tầng
Tham chiếu sản phẩm
CodeSignal · Moodle · Gradescope · Aced
Mục lục (11 phần)
  1. Bản đồ hệ thống
  2. Quy tắc giữa các module
  3. M1 · Identity
  4. M2 · Assessment & Quiz
  5. M3 · Chấm tự động
  6. M4 · AI
  7. M5 · Điểm & kỹ năng
  8. Frontend
  9. Hạ tầng & CI/CD
  10. Một lượt làm bài
  11. Thứ tự xây

§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.

Web App React 19 · TypeScript Caddy HTTPS proxy API SERVER · ASP.NET CORE 10 M1 · Identity & Platform JWT · RBAC · audit · rate limit M2 · Assessment & Quiz kỹ năng · đề · câu hỏi · rubric · chấm quiz M3 · Submission lượt làm · bài nộp · trạng thái · enqueue M4 · AI Evaluation & Interview IAiProvider · prompt · phỏng vấn M5 · Scoring & Skill Profile điểm 4 tầng · kỹ năng · gợi ý học IEventBus · outbox · sự kiện trong tiến trình SignalR hub /hubs/submissions Hangfire enqueue · dashboard SQL Server dữ liệu 5 schema · Hangfire storage MinIO (S3) file bài nộp · log chạy test Asap.Worker Hangfire server · điều phối Docker Engine container bài làm · no network MÁY WORKER RIÊNG OpenAI / Gemini qua Microsoft.Extensions.AI HTTPS REST EF Core upload lấy job tải file Docker.DotNet chấm code phỏng vấn WebSocket · trạng thái chấm
Khung đỏ là nơi duy nhất mã sinh viên được chạy. Worker báo tiến độ qua một endpoint nội bộ của API (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ầngChọnVì sao
FrontendReact 19 + TypeScript + ViteChi 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
DatabaseSQL Server 2022, mỗi module một schemaĐúng đề xuất gốc; schema riêng giữ ranh giới module
Kho fileMinIO (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 đợiHangfire + storage SQL ServerCó sẵn retry, dashboard, tự trả job khi worker chết; không cần thêm Redis
Thời gian thựcSignalRĐẩy trạng thái chấm, dự phòng bằng polling
SandboxDocker Engine + Docker.DotNetĐiều khiển container từ C#
AIMicrosoft.Extensions.AI (IChatClient)Một interface cho OpenAI, Gemini và bản giả lập khi test
ProxyCaddyTự cấp chứng chỉ HTTPS, cấu hình vài dòng
Giám sátSerilog + Seq, health checksTra log theo submissionId
Hai điểm chỉnh so với bản phân tích

(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.

  1. Mỗi module sở hữu dữ liệu của mình. Một DbContext và một schema riêng (identity, assess, submit, ai, scoring). Không join chéo schema.
  2. Gọi đồng bộ chỉ qua project Contracts. Module A chỉ được tham chiếu Asap.Modules.B.Contracts (interface + DTO), không bao giờ tham chiếu phần bên trong của B.
  3. 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ì

ModuleCung cấp (Contracts)Dùng của module khácPhát sự kiệnNghe sự kiện
M1 IdentityICurrentUser, IUserDirectoryUserCreated
M2 AssessmentIAssessmentReader (bản đã publish, test case, rubric, ItemSkill)M1AssessmentPublished, EvaluationRecorded (quiz)AttemptStarted
M3 SubmissionIAttemptReaderM1, M2AttemptStarted, AttemptFinished, EvaluationRecorded (test, rule)
M4 AIIAiEvaluator (worker gọi trực tiếp)M1, M2EvaluationRecorded (AI), ReviewRequestedAttemptFinished
M5 ScoringIResultReader, ISkillProfileReaderM1, M2ResultFinalized, SkillScoreChangedEvaluationRecorded, 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ệnDùng đểGhi chú
FluentValidationKiểm tra dữ liệu đầu vàoTrả lỗi dạng ProblemDetails (RFC 9457)
MapperlyMap entity ↔ DTOSinh code lúc build, giấy phép MIT
Dispatcher tự viếtPhát sự kiện nội bộ + outboxKhoảng 100 dòng code; MediatR và AutoMapper đã chuyển sang giấy phép thương mại từ 2025
Microsoft.AspNetCore.OpenApi + ScalarSinh tài liệu OpenAPI và trang thử APIFrontend sinh client TypeScript từ file này
NetArchTestTest kiến trúcFail CI nếu module tham chiếu phần trong của module khác

§3M1 · Identity & Platform SV1

SchemaidentityThực thểUser, Role, Permission, RefreshToken, AuditLogKiêmCI/CD, triển khai

Trả lời hai câu hỏi cho mọi request: ai đang gọihọ 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

POST /auth/login Kiểm tra mật khẩu (PBKDF2) Access token JWT · 15 phút → body Refresh token · 7 ngày → cookie httpOnly Frontend giữ access token trong bộ nhớ
API trả 401 POST /auth/refresh (cookie tự gửi) Cấp cặp token mới, thu hồi token cũ Token cũ bị dùng lại → thu hồi cả chuỗi

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

Request Rate limiter Authentication (JwtBearer) Authorization (policy) Endpoint SaveChanges interceptor → AuditLog

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ềnAdminInstructorStudent
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ầnCông nghệViệc
UserStore, PasswordHasherASP.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
TokenServiceMicrosoft.IdentityModel.JsonWebTokensKý JWT (HMAC-SHA256 hoặc RSA), claim sub, role, perm
RefreshTokenServiceEF CoreLưu hash của token, xoay vòng, phát hiện dùng lại
PermissionPolicyProviderASP.NET Core AuthorizationTự tạo policy từ tên quyền: RequirePermission("result.override")
Rate limitingMicrosoft.AspNetCore.RateLimiting (có sẵn)Login 5 lần/phút/IP; API chung 100 lần/phút/người
AuditInterceptorEF Core SaveChangesInterceptorGhi ai, lúc nào, IP, entity, giá trị trước/sau cho các entity đánh dấu [Audited]
CurrentUserContracts: ICurrentUserModule 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

SchemaassessThực thểSkill, Assessment, Section, Item, ItemSkill, Question(Version), Option, TestCase, Rubric, QuizConfig, AttemptQuestion, AttemptAnswer

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á

Draft Kiểm tra cấu hình (tổng trọng số = 100%, đủ câu cho ma trận) Dry-run lời giải mẫu đạt 100% Published v1 · bất biến Giao lớp
Published v1 Sửa Draft v2 (bản sao) … publish v2; bài đã làm vẫn giữ v1

Luồng 2 — Một lượt làm trắc nghiệm

Trình duyệt SV M2 · Quiz API SQL Server ① POST /quiz/start ② đọc QuizConfig + ngân hàng câu hỏi ③ PaperGenerator: rút theo ma trận, trộn câu và lựa chọn ④ lưu AttemptQuestion (đề cố định cho lượt này) ⑤ đề KHÔNG kèm đáp án + deadline ⑥ PUT /answers/{q} — tự lưu mỗi lần chọn ⑦ upsert AttemptAnswer (từ chối nếu quá hạn) ⑧ POST /quiz/submit (hoặc job tự nộp) ⑨ QuizScorer theo chính sách chấm của từng câu ⑩ lưu điểm + outbox: EvaluationRecorded ⑪ điểm + giải thích (theo ShowAnswerPolicy)
Trắc nghiệm chấm trong vài mili-giây nên chạy đồng bộ, không qua worker. Nhưng kết quả vẫn đi ra bằng đúng sự kiện 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ầnCông nghệViệc
QuestionBankServiceEF Core, bảng QuestionVersion chỉ INSERTTạo, sửa (sinh phiên bản mới), tìm theo kỹ năng và độ khó
QuestionImporterClosedXML (Excel), parser GIFT tự viếtNhập hàng loạt; báo lỗi theo từng dòng
BlueprintValidatorC# thuầnKiểm tra ngân hàng đủ câu cho từng ô của ma trận đề
PaperGeneratorC# thuần, Random có seedRút câu theo ma trận, trộn thứ tự, không trùng câu
QuizScorer + IScoringStrategyStrategy patternAllOrNothing, Partial, PartialWithPenalty
TestCaseParserYamlDotNet + JsonSchema.NetĐọc YAML test case REST, kiểm tra đúng schema trước khi lưu
RubricValidatorFluentValidationTổng trọng số = 100%, mỗi tiêu chí có nguồn chấm và mô tả mức
DryRunServiceGọi M3 qua ContractsNộ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 · ItemAnalysisJobHangfire recurringTự 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 JSONEF 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

SchemasubmitThực thểAttempt, Submission, ExecutionJob, TestResultTiến trìnhmodule trong API + Asap.Worker

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.

M3 TRONG API POST /submissionstrả 202 ngay Lưu fileMinIO · sha256 · ≤ 20 MB Status = Queuedcùng transaction + outbox EnqueueHangfire · queue grading Worker nhận jobkiểm tra trạng thái · khoá bản ghi job BUILD · CHỈ RA REGISTRY · 180 S docker buildDockerfile của SV · base image có sẵn RUN · KHÔNG INTERNET · 1 CPU · 512 MB · 120 S Chạy containermạng nội bộ của job Health checkGET /health ≤ 60 s HTTP test runnerYAML · JSONPath · capture tải file bằng URL ký sẵn, build image Rule checkstrên lưu lượng HTTP đã ghi AI reviewM4 · IAiEvaluator Ghi kết quảTestResult + EvaluationRecorded bản ghi request/response điểm luật luôn chạy cuối: xoá container, image, mạng và thư mục của job
Bài lập trình stdin/stdout đi cùng đường nhưng ngắn hơn: build (nếu cần biên dịch) → chạy từng test trong container --network noneOutputComparator 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ầnCông nghệViệc
SubmissionServiceAPI · EF CoreKiểm tra chủ lượt làm, hạn nộp, dung lượng; lưu file; đặt Queued; ghi outbox
ArtifactStoreMinio .NET SDKLưu file theo submissionId, tạo URL ký sẵn sống 10 phút cho worker
GradingJobHangfire · [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)
DockerRunnerDocker.DotNetTạo mạng Internal = true, build từ tar, tạo container với Memory, NanoCPUs, PidsLimit, ReadonlyRootfs, CapDrop = ALL, kill khi quá giờ
HealthProbeMicrosoft.Extensions.Http.ResilienceGọi /health mỗi 2 giây, tối đa 60 giây
HttpTestRunnerHttpClient + JsonPath.Net + JsonSchema.NetChạy test tuần tự, thay biến đã capture, ghi lại toàn bộ request/response
OutputComparatorC# thuầnSo output bài stdin/stdout: bỏ khoảng trắng cuối, sai số số thực
RuleEngineMỗi luật một class IApiRuleChấ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
ProgressReporterHttpClient → endpoint nội bộ của APIBá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/submissions202 · 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.

Cần thử sớm (spike tuần 2)

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

SchemaaiThực thểAiProviderConfig, PromptTemplate, InterviewSession, InterviewTurn, AiEvaluation, GoldSetItem

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

Rubrictiêu chí · mô tả mức Bài làmđặt trong delimiter PromptBuilderPromptTemplate v3 Lần chấm 1temperature 0 Lần chấm 2gọi độc lập Schema + độ lệchJSON Schema · Δ ≤ 1.5 Bằng chứngtrích dẫn có trong bài EvaluationRecordednguồn: AI NeedsReviewGV duyệt → nguồn Human đạt lệch / sai schema thiếu
Nét đứt là nhánh không tin được kết quả AI. Sai schema được gọi lại một lần trước khi chuyển sang giảng viên. Khi giảng viên duyệt hoặc sửa, M5 nhận một EvaluationRecorded mới có nguồn Human.

Luồng 2 — Một phiên phỏng vấn

Lấy câu gốc từ ngân hàng M2 theo kỹ năng AI hỏi SV trả lời FollowUpDecider: hỏi nối tiếp (tối đa 2) hay sang câu mới Hết 5 câu Chấm theo luồng 1 (job nền) Chờ GV duyệt

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ầnCông nghệViệc
Chat clientMicrosoft.Extensions.AI · IChatClient + Microsoft.Extensions.AI.OpenAIMột interface cho OpenAI; Gemini qua endpoint tương thích OpenAI; FakeChatClient cho test và CI
Structured outputGetResponseAsync<T>() + JsonSchema.NetBuộc AI trả đúng kiểu C#; kiểm tra lại bằng schema trước khi tin
PromptTemplateStoreEF Core, có phiên bảnMỗi lần chấm lưu lại dùng template phiên bản nào
EvaluationGuardC# thuầnSchema, độ lệch hai lần, bằng chứng có thật trong bài, cờ nghi prompt injection
InterviewEngineMáy trạng tháiAsking → Awaiting → FollowUp(n) → Next → Completed
ResilienceMicrosoft.Extensions.Http.ResilienceRetry 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 · AiCallLogEF CoreGiới hạn token mỗi bài; lưu model, token, thời gian, response gốc
GoldSetRunnerTest 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

SchemascoringThực thểEvaluation, ItemResult, AssessmentResult, ScoreOverride, SkillScore, SkillSnapshot, RecommendationRule, LearningResource

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.

M2 · Quiz M3 · Test + Rule M4 · AI GV · ghi đè EvaluationRecorded Evaluationmỗi tiêu chí một bản ghi ItemResultΣ c × w theo rubric AssessmentResultΣ I × W Đủ item đã chốt?còn bài chờ GV duyệt? SkillScoreΣ I × m / Σ m SkillSnapshotlịch sử → biểu đồ Recommendationluật: kỹ năng < 60 có → ResultFinalized
Mọi bước từ Evaluation trở đi là hàm thuần: luôn tính lại từ đầu từ các bản ghi Evaluation, không cộng dồn. Nhờ vậy khi giảng viên ghi đè một tiêu chí, chỉ cần phát sự kiện và toàn bộ chuỗi tự đúng lại; chạy lại bao nhiêu lần cũng ra cùng kết quả.

Thành phần và công nghệ

Thành phầnCông nghệViệc
EvaluationProjectionHandler sự kiện · EF CoreGhi 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 CalculatorC# thuầnCông thức tầng 2 và 3; trả kèm cây truy vết
FinalizationPolicyC# thuầnChỉ chốt khi mọi item có điểm và không còn tiêu chí NeedsReview
SkillScoreCalculatorC# thuầnTầ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ị
SnapshotWriterEF CoreChỉ ghi snapshot khi điểm thay đổi
RecommendationRulesBả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"
OverrideServiceM1 auditBắt buộc lý do; phát ScoreOverridden và một Evaluation nguồn Human
ReportExporterCsvHelperXuấ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ệcThư việnGhi chú
Nền tảngReact 19 + TypeScript + Vite
Điều hướngReact RouterNhánh route theo vai trò, tải lười từng module
Gọi API, cacheTanStack Query + openapi-typescript + openapi-fetchKiể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ệnAnt DesignBảng, form, layout quản trị có sẵn
Formreact-hook-form + zodForm soạn đề, rubric nhiều trường
Soạn code, YAML@monaco-editor/reactDùng cho cả ô viết code của SV và ô soạn test case YAML của GV
Biểu đồRechartsRadar kỹ năng, đường tiến bộ
Thời gian thực@microsoft/signalrTrạ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 · PlaywrightMSW 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ồngCách làm
Hết hạn tokenInterceptor 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ệmMỗ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ấmuseSubmissionStatus 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àiExamLayout ẩ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áyDịch vụ (Docker Compose)Cấu hình đề xuất
Máy chínhcaddy (HTTPS + phục vụ file tĩnh của web) · api (kèm Hangfire server cho queue ai, maintenance) · sqlserver · minio · seq4 vCPU · 8 GB RAM
Máy workerworker (Hangfire server cho queue grading, 4 job song song) · Docker Engine · runner image dựng sẵn4 vCPU · 8 GB RAM; chỉ mở kết nối ra SQL Server, MinIO, API nội bộ, AI provider

Pipeline

Pull request dotnet build · pnpm build Unit (xUnit, Vitest) Integration (Testcontainers) Kiến trúc (NetArchTest) Bộ bài độc hại Được merge
Merge vào main Build image api, worker, web Đẩy lên GHCR SSH: docker compose pull && up -d EF migration Smoke test /health

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/live/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ướcModuleViệc xảy raCông nghệ chính
1M2GV kiểm tra cấu hình, chạy lời giải mẫu, publish v1FluentValidation · dry-run qua M3
2M3An bắt đầu; tạo Attempt với deadlineOutbox → AttemptStarted
3M2Sinh đề, tự lưu, nộp, chấm 8/10PaperGenerator · QuizScorerEvaluationRecorded
4M3Nộp file zip dự án REST; trả 202MinIO · Hangfire queue grading
5M3 · WorkerBuild, chạy, health check, 20 test HTTP, luật thiết kếDocker.DotNet · JsonPath.Net
6M4 · từ WorkerAI nhận xét chất lượng mã, qua bộ kiểm soátMicrosoft.Extensions.AI · EvaluationGuard
7M3 → WebGiao diện thấy "Chạy test HTTP 12/20"Endpoint nội bộ → SignalR
8M4Phỏng vấn 5 câu, AI hỏi nối tiếp; chấm nềnInterviewEngine · Hangfire queue ai
9M4 · M5GV duyệt điểm phỏng vấnScoreOverridden · AuditLog
10M5Tính 80 / 79.25 / 76.25 → 78.5; chốt kết quảCalculator thuần → ResultFinalized
11M5Cập nhật REST API 78, Security 55; lưu snapshot; gợi ý học SecuritySkillScoreChanged · RecommendationRules
12WebAn xem kết quả, bấm vào từng tiêu chí, xem hồ sơ kỹ năngTanStack 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:

SpikeNgườiCâu hỏi cần trả lời
Docker từ C#SV3Build 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ả JSONSV4Structured 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à cookieSV1Refresh token trong cookie httpOnly có chạy được giữa Vite dev server và API (CORS, SameSite) không?
Outbox và cột JSONSV2 · SV5Dispatcher 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ì

SprintM1 · SV1M2 · SV2M3 · SV3M4 · SV4M5 · SV5
S1 · T3–4Đăng nhập, RBACCRUD kỹ năng, bài đánh giáAttempt; worker nhận job thửIChatClient + bản giả lậpNhận EvaluationRecorded
S2 · T5–6Audit, rate limitTrắc nghiệm end-to-endUpload, MinIO, trạng tháiKho prompt có phiên bảnItemResult, AssessmentResult, trang kết quả v1
S3 · T7–8CI/CD, bản demo onlineNgân hàng câu hỏi, nhập ExcelSandbox, runner stdin/stdoutAI nhận xét mã + bộ kiểm soátAPI truy vết
S4 · T9–10Gia cố bảo mậtSoạn test case YAML, dry-runRunner REST, rule engineInterview engineSkillScore
S5 · T11–12Màn quản trị người dùngRút đề theo ma trậnRetry, DeadLetter, tiến độ SignalRMàn GV duyệt, gold setSnapshot, radar, biểu đồ tiến bộ
S6 · T13–14Màn audit logThống kê câu hỏiBộ bài độc hại hoàn chỉnhChống prompt injection, giới hạn tokenGợi ý học, dashboard lớp
S7 · T15–16Cả 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
Mốc cắt phạm vi

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.